using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing.Printing;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using IppServer.Host.Ipp;
using IppServer.Host.Printing;

namespace IppServer.Host
{
    internal sealed class IppServerHost : IDisposable
    {
        private const int MaxJobSizeBytes = 200 * 1024 * 1024; // 200 MB
        private static readonly TimeSpan PrintJobTimeout = TimeSpan.FromMinutes(3);
        private const int CompletedJobVisibleSeconds = 30;
        private const int CompletedJobRetentionSeconds = 3600; // 1 hour
        private const long MaxReplayJobSizeBytes = 50L * 1024 * 1024; // 50 MB
        private const int MaxJobsPerQueue = 200;
        private const int ProcessingStateHoldMilliseconds = 10000;
        
        private readonly HttpListener _listener = new HttpListener();
        private ServerConfig _config; // 改为可变，支持热更新
        private readonly object _configLock = new object(); // 配置更新锁
        private Configuration.IpMacRestriction _ipMacRestriction; // IP/MAC限制配置
        private readonly object _restrictionLock = new object(); // 限制配置更新锁
        private CancellationTokenSource _cts = new CancellationTokenSource(); // 改为非 readonly，支持重启时替换
        private readonly Action<string> _logCallback;
        private Task _listenerLoop = Task.CompletedTask;
        private readonly ConcurrentDictionary<string, SemaphoreSlim> _queueLocks = new ConcurrentDictionary<string, SemaphoreSlim>(StringComparer.OrdinalIgnoreCase);
        // 作业跟踪：记录最近完成的作业（用于 Get-Jobs 响应）
        private readonly ConcurrentDictionary<string, List<CompletedJob>> _completedJobs = new ConcurrentDictionary<string, List<CompletedJob>>(StringComparer.OrdinalIgnoreCase);
        // 日志存储（用于WEB界面显示）
        private readonly List<LogEntry> _logs = new List<LogEntry>();
        private readonly object _logsLock = new object();
        private const int MaxLogEntries = 1000; // 最多保存1000条日志
        private readonly ConcurrentDictionary<int, CancellationTokenSource> _jobCancellationTokens = new ConcurrentDictionary<int, CancellationTokenSource>();
        private readonly ConcurrentDictionary<int, string> _jobIdToQueue = new ConcurrentDictionary<int, string>();
        private readonly ConcurrentDictionary<string, DateTime> _queueProcessingHold = new ConcurrentDictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase);
        // 端口 80 简易重定向监听（可选）
        private HttpListener _port80Listener;
        private Task _port80ListenerLoop = Task.CompletedTask;
        // mDNS服务发现管理器
        private Discovery.MdnsServiceDiscovery _mdnsDiscovery;
        // 是否启用mDNS发现服务
        private readonly bool _enableMdnsDiscovery;
        // 私有 UDP 发现服务（始终启用）
        private readonly Discovery.PrivateDiscoveryServer _privateDiscovery;
        // 作业缓存目录（用于重打）
        private readonly string _jobCacheDirectory;
        private readonly string _jobHistoryFile;
        public event Action<bool> IppServiceStateChanged;
        private bool _ippServiceEnabled = true;
        private readonly object _serviceStateLock = new object();
        private readonly object _jobHistoryLock = new object();
        // Web管理界面
        private WebUI.AdminWebUI _adminWebUI;
        // 客户端辅助工具缓存
        private byte[] _clientToolBinary;
        private readonly object _clientToolLock = new object();
        
        /// <summary>
        /// 获取 AdminWebUI 实例（用于外部配置回调）
        /// </summary>
        public WebUI.AdminWebUI AdminWebUI => _adminWebUI;
        
        // TCP 9100 服务回调（由 MainForm 提供）
        private Func<bool> _getTcpServiceStateCallback;
        private Func<(bool Success, string Message)> _startTcpServiceCallback;
        private Func<(bool Success, string Message)> _stopTcpServiceCallback;
        private Func<List<Configuration.TcpPrinterTask>> _getTcpTasksCallback;
        private Func<Configuration.TcpPrinterTask, (bool Success, string Message)> _addTcpTaskCallback;
        private Func<int, (bool Success, string Message)> _removeTcpTaskCallback;
        private Func<int, Configuration.TcpPrinterTask, (bool Success, string Message)> _updateTcpTaskCallback;
        
        // TCP 作业跟踪（类似 IPP 作业）
        private readonly ConcurrentDictionary<int, TcpJob> _tcpJobs = new ConcurrentDictionary<int, TcpJob>();
        private readonly object _tcpJobsLock = new object();
        private int _nextTcpJobId = 1;
        
        public class TcpJob
        {
            public int JobId { get; set; }
            public int Port { get; set; }
            public string PrinterName { get; set; }
            public string ClientIp { get; set; }
            public string PrintMode { get; set; }
            public long DataSizeBytes { get; set; }
            public int JobState { get; set; } // 3=processing, 9=completed, 8=cancelled, 5=processing
            public string ErrorMessage { get; set; }
            public DateTime CreatedTime { get; set; }
            public DateTime CompletedTime { get; set; }
        }
        
        public class CompletedJob
        {
            public int JobId { get; set; }
            public int JobState { get; set; } = 3; // processing -> completed
            public string JobUri { get; set; }
            public string JobName { get; set; }
            public string UserName { get; set; }
            public string ClientIp { get; set; }
            public string QueueName { get; set; }
            public string PrinterName { get; set; }
            public string DocumentFormat { get; set; }
            public long DocumentSizeBytes { get; set; }
            public int PageCount { get; set; }
            public string PayloadPath { get; set; }
            public bool CanReplay { get; set; }
            public string ErrorMessage { get; set; }
            public DateTime CompletedTime { get; set; }
            public DateTime CreatedTime { get; set; }
            public DateTime ProcessingHoldUntil { get; set; } = DateTime.MinValue;
        }

        [DataContract]
        private class CompletedJobSnapshot
        {
            [DataMember] public int JobId { get; set; }
            [DataMember] public int JobState { get; set; }
            [DataMember] public string JobUri { get; set; }
            [DataMember] public string JobName { get; set; }
            [DataMember] public string UserName { get; set; }
            [DataMember] public string ClientIp { get; set; }
            [DataMember] public string QueueName { get; set; }
            [DataMember] public string PrinterName { get; set; }
            [DataMember] public string DocumentFormat { get; set; }
            [DataMember] public long DocumentSizeBytes { get; set; }
            [DataMember] public int PageCount { get; set; }
            [DataMember] public string PayloadPath { get; set; }
            [DataMember] public bool CanReplay { get; set; }
            [DataMember] public string ErrorMessage { get; set; }
            [DataMember] public DateTime CompletedTime { get; set; }
            [DataMember] public DateTime CreatedTime { get; set; }
        }
        
        private int _nextJobId = 1; // 作业ID计数器

        public IppServerHost(ServerConfig config, Action<string> logCallback = null, bool enableMdnsDiscovery = true)
        {
            _config = config;
            _logCallback = logCallback;
            _enableMdnsDiscovery = enableMdnsDiscovery;
            _listener.Prefixes.Add(config.BasePrefix);
            
            // 加载IP/MAC限制配置
            var settings = Configuration.AppSettingsStore.Load();
            lock (_restrictionLock)
            {
                _ipMacRestriction = settings.IpMacRestriction ?? Configuration.IpMacRestriction.CreateDefault();
            }
            
            // 添加根路径监听前缀，以便处理根路径请求并自动跳转到管理界面
            var rootPrefix = BuildRootPrefix(config.BasePrefix);
            if (!string.IsNullOrEmpty(rootPrefix) && rootPrefix != config.BasePrefix)
            {
                _listener.Prefixes.Add(rootPrefix);
            }

            // 作业缓存目录改为公共应用数据目录下：%ProgramData%\IppServer\job-cache
            var programData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
            var appDataDir = Path.Combine(programData, "IppServer");
            _jobCacheDirectory = Path.Combine(appDataDir, "job-cache");
            Directory.CreateDirectory(_jobCacheDirectory);

            _jobHistoryFile = Path.Combine(_jobCacheDirectory, "jobs-history.json");
            LoadJobHistory();
            
            // 初始化私有 UDP 发现服务，并同步当前配置（始终启用）
            _privateDiscovery = new Discovery.PrivateDiscoveryServer(Log);
            _privateDiscovery.UpdateConfig(_config.BasePrefix, _config.QueueToPrinter, GetTcpTasks());
            
            // 初始化Web管理界面（TCP 回调稍后通过 SetTcpServiceCallbacks 设置）
            _adminWebUI = new WebUI.AdminWebUI(
                _config,
                _listener,
                GetLocalIpAddresses,
                ExtractPortFromPrefix,
                ExtractPathFromPrefix,
                _completedJobs,
                _enableMdnsDiscovery,
                Log,
                ReloadConfig,
                StopJobAsync,
                ReplayJobAsync,
                DeleteJobAsync,
                GetLogs,
                ClearLogs,
                ExportLogs,
                () => IsIppServiceEnabled,
                StartIppService,
                StopIppService,
                RestartIppService,
                () => GetTcpServiceState(),
                () => StartTcpService(),
                () => StopTcpService(),
                () => GetTcpTasks(),
                (task) => AddTcpTask(task),
                (index) => RemoveTcpTask(index),
                (index, task) => UpdateTcpTask(index, task),
                () => GetTcpJobs()); // 传递配置重载回调、作业控制、日志和服务控制
        }
        
        /// <summary>
        /// 更新 AdminWebUI 的 TCP 服务回调（在 SetTcpServiceCallbacks 后调用）
        /// </summary>
        public void UpdateAdminWebUITcpCallbacks()
        {
            if (_adminWebUI != null)
            {
                _adminWebUI.ConfigureTcpServiceCallbacks(
                    () => GetTcpServiceState(),
                    () => StartTcpService(),
                    () => StopTcpService(),
                    () => GetTcpTasks(),
                    (task) => AddTcpTask(task),
                    (index) => RemoveTcpTask(index),
                    (index, task) => UpdateTcpTask(index, task),
                    () => GetTcpJobs());
            }
        }
        
        /// <summary>
        /// 设置 TCP 9100 服务回调（由 MainForm 调用）
        /// </summary>
        public void SetTcpServiceCallbacks(
            Func<bool> getState,
            Func<(bool Success, string Message)> start,
            Func<(bool Success, string Message)> stop,
            Func<List<Configuration.TcpPrinterTask>> getTasks,
            Func<Configuration.TcpPrinterTask, (bool Success, string Message)> addTask,
            Func<int, (bool Success, string Message)> removeTask,
            Func<int, Configuration.TcpPrinterTask, (bool Success, string Message)> updateTask)
        {
            _getTcpServiceStateCallback = getState;
            _startTcpServiceCallback = start;
            _stopTcpServiceCallback = stop;
            _getTcpTasksCallback = getTasks;
            _addTcpTaskCallback = addTask;
            _removeTcpTaskCallback = removeTask;
            _updateTcpTaskCallback = updateTask;
            // 更新 AdminWebUI 的回调
            UpdateAdminWebUITcpCallbacks();
        }
        
        /// <summary>
        /// 记录 TCP 打印作业
        /// </summary>
        public int RecordTcpJob(int port, string printerName, string clientIp, string printMode, long dataSize)
        {
            lock (_tcpJobsLock)
            {
                var jobId = _nextTcpJobId++;
                var job = new TcpJob
                {
                    JobId = jobId,
                    Port = port,
                    PrinterName = printerName,
                    ClientIp = clientIp,
                    PrintMode = printMode,
                    DataSizeBytes = dataSize,
                    JobState = 5, // processing
                    CreatedTime = DateTime.Now
                };
                _tcpJobs[jobId] = job;
                return jobId;
            }
        }
        
        /// <summary>
        /// 更新 TCP 作业状态
        /// </summary>
        public void UpdateTcpJobState(int jobId, int state, string errorMessage = null)
        {
            if (_tcpJobs.TryGetValue(jobId, out var job))
            {
                job.JobState = state;
                if (state == 9 || state == 8) // completed or cancelled
                {
                    job.CompletedTime = DateTime.Now;
                }
                if (!string.IsNullOrEmpty(errorMessage))
                {
                    job.ErrorMessage = errorMessage;
                }
            }
        }
        
        /// <summary>
        /// 获取所有 TCP 作业
        /// </summary>
        public List<TcpJob> GetTcpJobs()
        {
            lock (_tcpJobsLock)
            {
                return _tcpJobs.Values.OrderByDescending(j => j.CreatedTime).ToList();
            }
        }
        
        // TCP 服务回调包装方法
        private bool GetTcpServiceState()
        {
            return _getTcpServiceStateCallback?.Invoke() ?? false;
        }
        
        private (bool Success, string Message) StartTcpService()
        {
            return _startTcpServiceCallback?.Invoke() ?? (false, "TCP 服务回调未设置");
        }
        
        private (bool Success, string Message) StopTcpService()
        {
            return _stopTcpServiceCallback?.Invoke() ?? (false, "TCP 服务回调未设置");
        }
        
        private List<Configuration.TcpPrinterTask> GetTcpTasks()
        {
            return _getTcpTasksCallback?.Invoke() ?? new List<Configuration.TcpPrinterTask>();
        }
        
        private (bool Success, string Message) AddTcpTask(Configuration.TcpPrinterTask task)
        {
            return _addTcpTaskCallback?.Invoke(task) ?? (false, "TCP 服务回调未设置");
        }
        
        private (bool Success, string Message) RemoveTcpTask(int index)
        {
            return _removeTcpTaskCallback?.Invoke(index) ?? (false, "TCP 服务回调未设置");
        }
        
        private (bool Success, string Message) UpdateTcpTask(int index, Configuration.TcpPrinterTask task)
        {
            return _updateTcpTaskCallback?.Invoke(index, task) ?? (false, "TCP 服务回调未设置");
        }

        public bool IsIppServiceEnabled
        {
            get
            {
                lock (_serviceStateLock)
                {
                    return _ippServiceEnabled;
                }
            }
        }

        private void NotifyIppServiceStateChanged(bool state)
        {
            try
            {
                IppServiceStateChanged?.Invoke(state);
            }
            catch
            {
                // 忽略订阅方异常
            }
        }

        public (bool Success, string Message) StartIppService()
        {
            lock (_serviceStateLock)
            {
                if (_ippServiceEnabled)
                {
                    return (false, "IPP 服务已经处于运行状态。");
                }
                _ippServiceEnabled = true;
            }

            Log("IPP 服务已启动。");
            NotifyIppServiceStateChanged(true);
            return (true, "IPP 服务已启动。");
        }

        public (bool Success, string Message) StopIppService()
        {
            lock (_serviceStateLock)
            {
                if (!_ippServiceEnabled)
                {
                    return (false, "IPP 服务已经处于停止状态。");
                }
                _ippServiceEnabled = false;
            }

            Log("IPP 服务已停止。");
            NotifyIppServiceStateChanged(false);
            return (true, "IPP 服务已停止。");
        }

        public (bool Success, string Message) RestartIppService()
        {
            bool wasRunning;
            lock (_serviceStateLock)
            {
                wasRunning = _ippServiceEnabled;
                _ippServiceEnabled = false;
            }

            Log("IPP 服务正在重启...");
            NotifyIppServiceStateChanged(false);

            Task.Run(async () =>
            {
                await Task.Delay(1000).ConfigureAwait(false);
                lock (_serviceStateLock)
                {
                    _ippServiceEnabled = true;
                }
                Log("IPP 服务已重启。");
                NotifyIppServiceStateChanged(true);
            });

            return (true, wasRunning ? "IPP 服务正在重启。" : "IPP 服务已启动。");
        }

        public void Start()
        {
            lock (_serviceStateLock)
            {
                _ippServiceEnabled = false;
            }
            _listener.Start();
            _listenerLoop = Task.Run(ListenLoopAsync);
            
            // 获取本机IP地址
            var localIps = GetLocalIpAddresses();
            var port = ExtractPortFromPrefix(_config.BasePrefix);
            var path = ExtractPathFromPrefix(_config.BasePrefix);
            
            Log("=".PadRight(60, '='));
            Log("Web 管理服务已启动（IPP 服务可通过控制按钮单独启动）");
            Log("=".PadRight(60, '='));
            
            if (localIps.Count > 0)
            {
                Log($"监听地址（客户端可使用以下任一地址连接）：");
                foreach (var ip in localIps)
                {
                    Log($"  • http://{ip}:{port}{path}");
                    Log($"  • ipp://{ip}:{port}{path}");
                }
            }
            else
            {
                Log($"监听地址：{GetDisplayBasePrefix() ?? _config.BasePrefix}");
            }
            
            // 如果基础端口不是 80，则尝试在 80 端口上启动一个简易重定向监听，
            // 使客户端可以只输入 IP（http://ip/）即可跳转到实际状态页面。
            if (port != 80)
            {
                TryStartPort80RedirectListener(port, path);
            }
            
            if (_config.QueueToPrinter.Count > 0)
            {
                Log($"已共享的打印机队列：");
                foreach (var kvp in _config.QueueToPrinter)
                {
                    Log($"  • {kvp.Key} -> {kvp.Value}");
                    if (localIps.Count > 0)
                    {
                        // 显示所有IP地址的URL，每个URL单独一行
                        foreach (var ip in localIps)
                        {
                            Log($"    http://{ip}:{port}{path}{kvp.Key}");
                        }
                    }
                }
            }
            else
            {
                Log("警告：当前没有配置任何打印机队列");
            }
            Log("=".PadRight(60, '='));
            
            // 启动mDNS服务发现（如果启用）
            if (_enableMdnsDiscovery)
            {
                // 异步启动mDNS服务发现（避免阻塞UI线程）
                Task.Run(() => StartMdnsDiscovery(localIps, port, path));
            }
            else
            {
                Log("mDNS发现服务已禁用（用户配置）");
            }

            // 启动私有 UDP 发现服务（无需单独开关）
            try
            {
                _privateDiscovery?.Start();
            }
            catch (Exception ex)
            {
                Log($"启动私有发现服务时发生错误：{ex.Message}");
            }
        }
        
        /// <summary>
        /// 启动mDNS服务发现（在后台线程执行，避免阻塞UI）
        /// </summary>
        private void StartMdnsDiscovery(List<string> localIps, int port, string path)
        {
            try
            {
                // 准备打印机服务信息
                var printerServices = new List<Discovery.MdnsServiceDiscovery.PrinterServiceInfo>();
                foreach (var kvp in _config.QueueToPrinter)
                {
                    printerServices.Add(new Discovery.MdnsServiceDiscovery.PrinterServiceInfo
                    {
                        ServiceName = $"{kvp.Value} ({kvp.Key})",
                        QueueName = kvp.Key,
                        PrinterName = kvp.Value
                    });
                }
                
                // 创建并启动mDNS服务发现
                _mdnsDiscovery = new Discovery.MdnsServiceDiscovery(_logCallback);
                if (printerServices.Count > 0)
                {
                    _mdnsDiscovery.Start(printerServices, port, _logCallback);
                }
            }
            catch (Exception ex)
            {
                Log($"启动mDNS服务发现时发生错误：{ex.Message}");
            }
        }
        
        private static int ExtractPortFromPrefix(string prefix)
        {
            if (string.IsNullOrWhiteSpace(prefix))
            {
                return 8631;
            }

            try
            {
                // 优先使用正则表达式直接从字符串中提取端口号（更可靠，不受URI解析影响）
                // 支持格式：http://+:8631/printers/ 或 http://192.168.1.1:8631/printers/
                var match = System.Text.RegularExpressions.Regex.Match(prefix, @":(\d+)/");
                if (match.Success && int.TryParse(match.Groups[1].Value, out var parsedPort) && parsedPort > 0)
                {
                    return parsedPort;
                }
                
                // 如果正则表达式失败，尝试使用 Uri 解析
                var uri = new Uri(prefix);
                // Uri.Port 如果明确指定了端口（包括80和443），会返回实际端口号
                // 如果未指定端口，返回 -1（对于http默认80，https默认443，但Port属性返回-1）
                if (uri.Port > 0)
                {
                    return uri.Port;
                }
                
                // 如果 Uri.Port 返回 -1，说明使用的是默认端口，但我们配置的应该总是明确指定端口
                // 尝试从原始字符串中再次提取
                match = System.Text.RegularExpressions.Regex.Match(prefix, @":(\d+)");
                if (match.Success && int.TryParse(match.Groups[1].Value, out parsedPort) && parsedPort > 0)
                {
                    return parsedPort;
                }
                
                return 8631; // 默认值
            }
            catch
            {
                // 如果解析失败，尝试从字符串中提取端口号
                try
                {
                    var match = System.Text.RegularExpressions.Regex.Match(prefix, @":(\d+)/");
                    if (match.Success && int.TryParse(match.Groups[1].Value, out var parsedPort) && parsedPort > 0)
                    {
                        return parsedPort;
                    }
                }
                catch
                {
                    // 忽略
                }
                
                return 8631;
            }
        }
        
        private static string ExtractPathFromPrefix(string prefix)
        {
            if (string.IsNullOrEmpty(prefix))
            {
                return "/printers/";
            }
            
            try
            {
                // 先尝试使用 Uri 解析（适用于标准 URI）
                var uri = new Uri(prefix);
                var path = uri.AbsolutePath;
                // 确保路径以 / 开头和结尾
                if (!path.EndsWith("/", StringComparison.Ordinal))
                {
                    path += "/";
                }
                return path;
            }
            catch
            {
                // 如果 Uri 解析失败（例如包含 + 或 *），使用正则表达式或字符串操作提取路径
                // 格式：http://host:port/path/ 或 http://+:port/path/
                var match = System.Text.RegularExpressions.Regex.Match(prefix, @"https?://[^/]+(/.+)", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
                if (match.Success)
                {
                    var path = match.Groups[1].Value;
                    // 确保路径以 / 开头和结尾
                    if (!path.StartsWith("/", StringComparison.Ordinal))
                    {
                        path = "/" + path;
                    }
                    if (!path.EndsWith("/", StringComparison.Ordinal))
                    {
                        path += "/";
                    }
                    return path;
                }
                
                // 如果都失败了，返回默认值
                return "/printers/";
            }
        }
        
        private static string BuildRootPrefix(string prefix)
        {
            if (string.IsNullOrEmpty(prefix))
            {
                return "http://+:8631/";
            }
            
            try
            {
                // 提取协议、主机和端口，构建根路径前缀
                // 例如：http://+:8631/printers/ -> http://+:8631/
                var match = System.Text.RegularExpressions.Regex.Match(prefix, @"^(https?://[^/]+)/", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
                if (match.Success)
                {
                    return match.Groups[1].Value + "/";
                }
                
                // 如果正则表达式失败，尝试使用 Uri 解析
                var uri = new Uri(prefix);
                var scheme = uri.Scheme;
                var host = uri.Host;
                var port = uri.Port;
                
                // 如果原始前缀包含 + 或 *，保持通配符
                if (prefix.Contains("://+") || prefix.Contains("://*"))
                {
                    var wildcard = prefix.Contains("://+") ? "+" : "*";
                    return $"{scheme}://{wildcard}:{port}/";
                }
                
                // 否则使用解析出的主机
                return $"{scheme}://{host}:{port}/";
            }
            catch
            {
                // 如果解析失败，尝试使用正则表达式提取
                try
                {
                    var match = System.Text.RegularExpressions.Regex.Match(prefix, @"^(https?://[^/]+)/", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
                    if (match.Success)
                    {
                        return match.Groups[1].Value + "/";
                    }
                }
                catch
                {
                    // 忽略
                }
                
                return "http://+:8631/";
            }
        }
        
        private static List<string> GetLocalIpAddresses()
        {
            var ips = new List<string>();
            try
            {
                foreach (var ni in NetworkInterface.GetAllNetworkInterfaces())
                {
                    if (ni.OperationalStatus != OperationalStatus.Up)
                    {
                        continue;
                    }

                    if (ni.NetworkInterfaceType == NetworkInterfaceType.Loopback ||
                        ni.NetworkInterfaceType == NetworkInterfaceType.Tunnel)
                    {
                        continue;
                    }

                    var ipProps = ni.GetIPProperties();
                    foreach (var unicast in ipProps.UnicastAddresses)
                    {
                        var address = unicast.Address;
                        if (address.AddressFamily != AddressFamily.InterNetwork)
                        {
                            continue;
                        }

                        if (IPAddress.IsLoopback(address))
                        {
                            continue;
                        }

                        var text = address.ToString();
                        if (text.StartsWith("169.254.", StringComparison.Ordinal))
                        {
                            continue;
                        }

                        ips.Add(text);
                    }
                }
            }
            catch
            {
                // ignored
            }

            if (ips.Count == 0)
            {
                try
                {
                    var hostEntry = Dns.GetHostEntry(Dns.GetHostName());
                    foreach (var ip in hostEntry.AddressList)
                    {
                        if (ip.AddressFamily == AddressFamily.InterNetwork && !IPAddress.IsLoopback(ip))
                        {
                            ips.Add(ip.ToString());
                        }
                    }
                }
                catch
                {
                    // ignored
                }
            }

            if (ips.Count == 0)
            {
                ips.Add("127.0.0.1");
            }

            return ips.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
        }

        private async Task ListenLoopAsync()
        {
            var token = _cts.Token;

            while (!token.IsCancellationRequested)
            {
                HttpListenerContext context = null;

                try
                {
                    context = await _listener.GetContextAsync().ConfigureAwait(false);
                }
                catch (HttpListenerException ex) when (ex.ErrorCode == 995)
                {
                    // 正常关闭，不记录日志
                    break;
                }
                catch (ObjectDisposedException)
                {
                    // 正常关闭，不记录日志
                    break;
                }
                catch (Exception ex)
                {
                    Log($"接受连接失败：{ex.Message}");
                }

                if (context != null)
                {
                    _ = Task.Run(() => HandleRequestAsync(context, token), token);
                }
            }
        }

        /// <summary>
        /// 端口 80 简易重定向监听循环：
        /// 接收到任何请求后，将浏览器重定向到实际 Web 服务的状态页面（配置端口和路径）。
        /// </summary>
        private async Task ListenPort80LoopAsync()
        {
            var token = _cts.Token;

            while (!token.IsCancellationRequested)
            {
                HttpListenerContext context = null;

                try
                {
                    context = await _port80Listener.GetContextAsync().ConfigureAwait(false);
                }
                catch (HttpListenerException ex) when (ex.ErrorCode == 995)
                {
                    // 正常关闭，不记录日志
                    break;
                }
                catch (ObjectDisposedException)
                {
                    // 正常关闭，不记录日志
                    break;
                }
                catch (Exception ex)
                {
                    Log($"80 端口监听接受连接失败：{ex.Message}");
                }

                if (context != null)
                {
                    _ = Task.Run(() => HandlePort80RequestAsync(context, token), token);
                }
            }
        }

        private async Task HandleRequestAsync(HttpListenerContext context, CancellationToken token)
        {
            var request = context.Request;
            var response = context.Response;

            var absolutePath = request.Url.AbsolutePath;
            var path = absolutePath.TrimStart('/');
            var clientIp = request.RemoteEndPoint?.Address?.ToString() ?? "未知";

            try
            {
                // IP/MAC地址限制验证（对所有请求进行验证）
                Configuration.IpMacRestriction restriction;
                lock (_restrictionLock)
                {
                    restriction = _ipMacRestriction;
                }
                
                if (restriction != null && restriction.Enabled)
                {
                    // 尝试获取MAC地址
                    var clientMac = Configuration.IpMacValidator.GetMacAddressFromIp(clientIp);
                    
                    if (!Configuration.IpMacValidator.IsAllowed(restriction, clientIp, clientMac, out var denyReason))
                    {
                        Log($"[{clientIp}] 访问被拒绝：{denyReason}");
                        
                        // 对于GET请求（浏览器访问），返回HTML错误页面
                        if (string.Equals(request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
                        {
                            await WriteAccessDeniedPageAsync(response, denyReason, clientIp).ConfigureAwait(false);
                            return;
                        }
                        // 对于POST请求（IPP请求），直接关闭连接
                        else
                        {
                            response.Close();
                            return;
                        }
                    }
                }

                // 根路径自动跳转到管理界面（优先处理）
                if (string.Equals(request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase) && 
                    (absolutePath == "/" || absolutePath == ""))
                {
                    string rootPath;
                    lock (_configLock)
                    {
                        rootPath = ExtractPathFromPrefix(_config.BasePrefix);
                    }
                    var adminPath = rootPath.TrimEnd('/') + "/admin";
                    response.StatusCode = (int)HttpStatusCode.Redirect;
                    response.RedirectLocation = adminPath;
                    response.Close();
                    return;
                }

                // Web管理API（支持任意HTTP方法）
                // 动态识别API路径：/{prefix}/api/... 或 /api/...
                if (path.StartsWith("api/", StringComparison.OrdinalIgnoreCase) ||
                    System.Text.RegularExpressions.Regex.IsMatch(path, @"^[^/]+/api/", System.Text.RegularExpressions.RegexOptions.IgnoreCase))
                {
                    await _adminWebUI.HandleApiRequestAsync(request, response, token).ConfigureAwait(false);
                    return;
                }

                if (string.Equals(request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
                {
                    var normalizedPath = path.TrimStart('/').ToLower();
                    
                    // 管理界面路由 - 支持 /{prefix}/admin 和 /admin
                    // 动态识别路径格式：/{prefix}/admin 或 /admin
                    if (normalizedPath == "admin" || normalizedPath == "admin/" ||
                        System.Text.RegularExpressions.Regex.IsMatch(normalizedPath, @"^[^/]+/admin/?$", System.Text.RegularExpressions.RegexOptions.IgnoreCase))
                    {
                        await _adminWebUI.WriteAdminPageAsync(request, response).ConfigureAwait(false);
                        return;
                    }
                    
                    // 静态文件处理 - 支持 /webui/logo.png 或 /{prefix}/webui/logo.png
                    if (normalizedPath == "webui/logo.png" || 
                        System.Text.RegularExpressions.Regex.IsMatch(normalizedPath, @"^[^/]+/webui/logo\.png$", System.Text.RegularExpressions.RegexOptions.IgnoreCase))
                    {
                        var logoPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "WebUI", "logo.png");
                        if (File.Exists(logoPath))
                        {
                            var logoData = await Task.Run(() => File.ReadAllBytes(logoPath)).ConfigureAwait(false);
                            response.StatusCode = (int)HttpStatusCode.OK;
                            response.ContentType = "image/png";
                            response.ContentLength64 = logoData.Length;
                            await response.OutputStream.WriteAsync(logoData, 0, logoData.Length, token).ConfigureAwait(false);
                            return;
                        }
                        else
                        {
                            await WritePlainResponse(response, HttpStatusCode.NotFound, "Logo file not found").ConfigureAwait(false);
                            return;
                        }
                    }

                    // 客户端辅助工具下载 - 支持 /clienttools/ipp-printer-install-helper.exe 或 /{prefix}/clienttools/ipp-printer-install-helper.exe
                    if (normalizedPath == "clienttools/ipp-printer-install-helper.exe" ||
                        System.Text.RegularExpressions.Regex.IsMatch(normalizedPath, @"^[^/]+/clienttools/ipp-printer-install-helper\.exe$", System.Text.RegularExpressions.RegexOptions.IgnoreCase))
                    {
                        var helperData = await Task.Run(() => LoadClientToolBinary()).ConfigureAwait(false);
                        if (helperData != null && helperData.Length > 0)
                        {
                            response.StatusCode = (int)HttpStatusCode.OK;
                            response.ContentType = "application/octet-stream";
                            response.ContentLength64 = helperData.Length;
                            response.Headers["Content-Disposition"] = "attachment; filename=\"IppPrinterInstallHelper.exe\"";
                            await response.OutputStream.WriteAsync(helperData, 0, helperData.Length, token).ConfigureAwait(false);
                            return;
                        }
                        else
                        {
                            await WritePlainResponse(response, HttpStatusCode.NotFound, "客户端辅助工具未嵌入到主程序，请重新构建或联系管理员。").ConfigureAwait(false);
                            return;
                        }
                    }
                    
                    // API端点 - 支持 /printers/api/ 和 /api/
                    // 检查是否是队列路径（/printers/queue-name）
                    var queueNameForGet = ExtractQueueName(request.Url);
                    var isTest = string.Equals(request.QueryString["test"], "1", StringComparison.OrdinalIgnoreCase);

                    // 线程安全地获取当前配置
                    ServerConfig currentConfigForTest;
                    lock (_configLock)
                    {
                        currentConfigForTest = _config;
                    }
                    
                    if (isTest && !string.IsNullOrEmpty(queueNameForGet) &&
                        currentConfigForTest.QueueToPrinter.TryGetValue(queueNameForGet, out var printerForTest))
                    {
                        if (!IsIppServiceEnabled)
                        {
                            await WritePlainResponse(response, HttpStatusCode.ServiceUnavailable, "IPP 服务已停止，暂时无法执行测试打印。").ConfigureAwait(false);
                            return;
                        }

                        await HandleWebTestPrintAsync(request, response, printerForTest, queueNameForGet, token).ConfigureAwait(false);
                        return;
                    }

                    // 如果是配置的路径前缀但没有队列名，显示状态页
                    // 从配置中获取路径前缀
                    string configuredPath;
                    lock (_configLock)
                    {
                        configuredPath = ExtractPathFromPrefix(_config.BasePrefix);
                    }
                    if (configuredPath.EndsWith("/"))
                    {
                        configuredPath = configuredPath.TrimEnd('/');
                    }
                    if (absolutePath == configuredPath || absolutePath == configuredPath + "/")
                    {
                        await WriteStatusPageAsync(response).ConfigureAwait(false);
                        return;
                    }

                    // 如果有队列名，显示状态页（兼容旧版）
                    if (!string.IsNullOrEmpty(queueNameForGet))
                    {
                        await WriteStatusPageAsync(response).ConfigureAwait(false);
                        return;
                    }

                    // 其他情况，显示管理界面
                    await _adminWebUI.WriteAdminPageAsync(request, response).ConfigureAwait(false);
                    return;
                }

                if (!string.Equals(request.HttpMethod, "POST", StringComparison.OrdinalIgnoreCase))
                {
                    await WritePlainResponse(response, HttpStatusCode.MethodNotAllowed, "仅支持 POST 请求。").ConfigureAwait(false);
                    return;
                }

                var queueName = ExtractQueueName(request.Url);
                
                // IP/MAC限制验证已在前面统一处理，这里不再重复验证
                
                var payload = await ReadBodyAsync(request.InputStream).ConfigureAwait(false);
                var ippRequest = IppRequestParser.Parse(payload);
                
                // 线程安全地获取当前配置
                ServerConfig currentConfig;
                lock (_configLock)
                {
                    currentConfig = _config;
                }
                
                if (!currentConfig.QueueToPrinter.TryGetValue(queueName, out var printerName))
                {
                    // 记录详细信息：IPP操作类型、完整URL、当前配置的队列列表
                    // 这通常是因为客户端之前安装了使用旧队列名的打印机，Windows后台轮询导致的
                    var operationName = ippRequest.Operation.ToString();
                    var fullUrl = request.Url.ToString();
                    var availableQueues = string.Join(", ", currentConfig.QueueToPrinter.Keys);
                    if (string.IsNullOrEmpty(availableQueues))
                    {
                        availableQueues = "(无)";
                    }
                    Log($"[{clientIp}] 访问未知队列：{queueName} | IPP操作：{operationName} (0x{(ushort)ippRequest.Operation:X4}) | URL：{fullUrl} | 当前可用队列：{availableQueues}");
                    await WritePlainResponse(response, HttpStatusCode.NotFound, $"未知的打印队列：{queueName}").ConfigureAwait(false);
                    return;
                }

                if (!IsIppServiceEnabled)
                {
                    Log($"[{clientIp}] 请求被拒绝：IPP 服务已停止");
                    await WritePlainResponse(response, HttpStatusCode.ServiceUnavailable, "IPP 服务已停止，暂时无法处理打印请求。").ConfigureAwait(false);
                    return;
                }

                // 诊断日志：记录收到的请求
                Log($"[{clientIp}] 收到 IPP 请求：操作={ippRequest.Operation} (0x{(ushort)ippRequest.Operation:X4}), 请求ID={ippRequest.RequestId}, 版本={ippRequest.Version >> 8}.{ippRequest.Version & 0xFF}, 队列={queueName}");
                
                // 记录所有请求的详细信息（用于诊断 Windows 客户端行为）
                if (ippRequest.Operation == IppOperation.PrintJob || ippRequest.Operation == IppOperation.GetJobs || ippRequest.Operation == IppOperation.GetJobAttributes)
                {
                    var requestDetails = new StringBuilder();
                    requestDetails.AppendLine($"[{clientIp}] 请求详情：");
                    requestDetails.AppendLine($"  操作：{ippRequest.Operation}");
                    requestDetails.AppendLine($"  请求ID：{ippRequest.RequestId}");
                    requestDetails.AppendLine($"  版本：{ippRequest.Version >> 8}.{ippRequest.Version & 0xFF}");
                    requestDetails.AppendLine($"  队列：{queueName}");
                    if (ippRequest.Operation == IppOperation.PrintJob)
                    {
                        var jobName = ExtractAttributeValue(ippRequest, "job-name");
                        requestDetails.AppendLine($"  作业名称：{jobName ?? "(未指定)"}");
                    }
                    Log(requestDetails.ToString());
                }
                
                // 如果是 Get-Job-Attributes 请求，记录请求的属性
                if (ippRequest.Operation == IppOperation.GetJobAttributes)
                {
                    var requestedJobId = ExtractAttributeValue(ippRequest, "job-id");
                    var requestedAttributes = ExtractRequestedAttributes(ippRequest);
                    Log($"[{clientIp}] Get-Job-Attributes 详情：job-id={requestedJobId ?? "(未指定)"}, requested-attributes={string.Join(", ", requestedAttributes ?? new List<string> { "all" })}");
                }

                // 构建打印机 URI（用于响应）
                var requestUri = request.Url;
                var printerUri = $"{requestUri.Scheme}://{requestUri.Host}:{requestUri.Port}{requestUri.AbsolutePath}";

                switch (ippRequest.Operation)
                {
                    case IppOperation.PrintJob:
                        await HandlePrintJobAsync(response, ippRequest, printerName, queueName, clientIp, printerUri, token).ConfigureAwait(false);
                        break;
                    case IppOperation.GetPrinterAttributes:
                        await HandleGetPrinterAttributesAsync(request, response, ippRequest, printerName, queueName).ConfigureAwait(false);
                        break;
                    case IppOperation.ValidateJob:
                        await WriteIppResponseAsync(response, ippRequest.RequestId, ippRequest.Version, 0x0000).ConfigureAwait(false);
                        break;
                    case IppOperation.CreateJob:
                        await HandleCreateJobAsync(response, ippRequest, printerName, queueName).ConfigureAwait(false);
                        break;
                    case IppOperation.SendDocument:
                        await HandleSendDocumentAsync(response, ippRequest, printerName, queueName, clientIp, token).ConfigureAwait(false);
                        break;
                    case IppOperation.GetJobs:
                        await HandleGetJobsAsync(response, ippRequest, printerName, queueName, clientIp).ConfigureAwait(false);
                        break;
                    case IppOperation.GetJobAttributes:
                        await HandleGetJobAttributesAsync(response, ippRequest, printerName, queueName, clientIp).ConfigureAwait(false);
                        break;
                    case IppOperation.CancelJob:
                        await HandleCancelJobAsync(response, ippRequest, queueName, clientIp).ConfigureAwait(false);
                        break;
                    default:
                        await WriteIppResponseAsync(response, ippRequest.RequestId, ippRequest.Version, 0x0501).ConfigureAwait(false);
                        break;
                }
            }
            catch (InvalidDataException ex)
            {
                Log($"[{clientIp}] 请求解析失败：{ex.Message}");
                await WritePlainResponse(response, HttpStatusCode.BadRequest, ex.Message).ConfigureAwait(false);
            }
            catch (OperationCanceledException)
            {
                Log($"[{clientIp}] 打印任务被取消");
                await WritePlainResponse(response, HttpStatusCode.ServiceUnavailable, "打印任务已取消。").ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                Log($"[{clientIp}] 处理请求失败：{ex.GetType().Name} - {ex.Message}");
                response.StatusCode = (int)HttpStatusCode.InternalServerError;
                try
                {
                    await WritePlainResponse(response, HttpStatusCode.InternalServerError, $"服务器错误：{ex.Message}").ConfigureAwait(false);
                }
                catch
                {
                    // 忽略响应写入错误
                }
            }
            finally
            {
                response.Close();
            }
        }

        /// <summary>
        /// 处理来自 80 端口的 HTTP 请求，将其重定向到实际 Web 服务的状态页面。
        /// </summary>
        private async Task HandlePort80RequestAsync(HttpListenerContext context, CancellationToken token)
        {
            var request = context.Request;
            var response = context.Response;

            try
            {
                string basePrefix;
                lock (_configLock)
                {
                    basePrefix = _config.BasePrefix;
                }

                // 解析目标端口和路径
                var targetPort = ExtractPortFromPrefix(basePrefix);
                var targetPath = ExtractPathFromPrefix(basePrefix);
                if (!targetPath.StartsWith("/"))
                {
                    targetPath = "/" + targetPath;
                }
                // 状态页为配置路径本身（例如 /ipp/ 或 /printers/），不附加队列名
                var uriBuilder = new UriBuilder
                {
                    Scheme = Uri.UriSchemeHttp,
                    Host = request.Url.Host,
                    Port = targetPort,
                    Path = targetPath.TrimEnd('/'),
                };

                var targetUrl = uriBuilder.Uri.ToString();

                response.StatusCode = (int)HttpStatusCode.Redirect;
                response.RedirectLocation = targetUrl;
                response.Close();
            }
            catch (Exception ex)
            {
                Log($"80 端口重定向处理失败：{ex.Message}");
                try
                {
                    await WritePlainResponse(response, HttpStatusCode.InternalServerError, $"80 端口重定向失败：{ex.Message}").ConfigureAwait(false);
                }
                catch
                {
                    // 忽略写入响应失败
                }
            }
        }

        /// <summary>
        /// 尝试在 80 端口上启动一个简易重定向监听。
        /// 如果端口被占用或权限不足，则记录日志但不抛出异常。
        /// </summary>
        /// <param name="targetPort">实际 Web 服务端口</param>
        /// <param name="targetPath">实际 Web 服务路径前缀</param>
        private void TryStartPort80RedirectListener(int targetPort, string targetPath)
        {
            // 避免重复启动
            if (_port80Listener != null && _port80Listener.IsListening)
            {
                return;
            }

            try
            {
                _port80Listener = new HttpListener();
                _port80Listener.Prefixes.Add("http://*:80/");
                _port80Listener.Start();
                _port80ListenerLoop = Task.Run(ListenPort80LoopAsync);

                Log($"已在 80 端口启动 HTTP 重定向服务，访问 http://<IP>/ 将自动跳转到 http://<IP>:{targetPort}{targetPath}");
            }
            catch (HttpListenerException ex)
            {
                Log($"80 端口监听失败（可能已被占用或权限不足），不启用 80 端口自动跳转：{ex.Message}");
                _port80Listener = null;
            }
            catch (Exception ex)
            {
                Log($"80 端口监听失败，不启用 80 端口自动跳转：{ex.Message}");
                _port80Listener = null;
            }
        }

        private async Task HandlePrintJobAsync(HttpListenerResponse response, IppRequest ippRequest, string printerName, string queueName, string clientIp, string printerUri, CancellationToken token)
        {
            // 检查文档数据
            if (ippRequest.DocumentData == null || ippRequest.DocumentData.Length == 0)
            {
                Log($"[{clientIp}] 打印任务失败：队列 '{queueName}' -> 打印机 '{printerName}'，原因：文档数据为空");
                await WriteIppResponseAsync(response, ippRequest.RequestId, ippRequest.Version, 0x0409).ConfigureAwait(false);
                return;
            }
            
            if (ippRequest.DocumentData.Length > MaxJobSizeBytes)
            {
                var limitStr = FormatFileSize(MaxJobSizeBytes);
                Log($"[{clientIp}] 打印任务被拒绝：队列 '{queueName}' -> 打印机 '{printerName}'，原因：文件过大（超过 {limitStr}）");
                await WriteIppResponseAsync(response, ippRequest.RequestId, ippRequest.Version, 0x0409).ConfigureAwait(false);
                return;
            }

            var jobName = ExtractAttributeValue(ippRequest, "job-name");
            var userName = ExtractAttributeValue(ippRequest, "requesting-user-name") ?? "未知用户";
            var fileSize = ippRequest.DocumentData.Length;
            var fileSizeStr = FormatFileSize(fileSize);

            // 提取文档信息
            var docFormat = ExtractAttributeValue(ippRequest, "document-format");
            if (string.IsNullOrEmpty(docFormat) || docFormat == "application/octet-stream")
            {
                docFormat = DetectFileFormat(ippRequest.DocumentData, jobName);
            }
            
            // 记录打印任务信息
            Log($" 收到客户端{clientIp}的请求的打印任务：");
            Log($"  客户端IP：{clientIp}/客户端用户{userName}");
            Log($"  文档名：{jobName ?? "(未指定)"}");
            Log($"  文件大小：{fileSizeStr} ({fileSize:N0} 字节)");
            Log($"  文档格式：{docFormat}");
            Log($"  目标：队列 '{queueName}' -> 打印机 '{printerName}'");

            var queueLock = GetQueueLock(queueName);
            var lockAcquired = false;
            CancellationTokenSource timeoutCts = null;
            try
            {
                var docName = $"IPP Job {queueName} {DateTime.Now:yyyyMMddHHmmss}";
                
                await queueLock.WaitAsync(token).ConfigureAwait(false);
                lockAcquired = true;

                timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(token);
                timeoutCts.CancelAfter(PrintJobTimeout);
                
                // 根据 IPP 规范和 SharpIppNext 的实现，Print-Job 响应应该返回 processing (5) 状态
                // 客户端会通过 Get-Jobs 或 Get-Job-Attributes 查询状态更新
                var jobId = System.Threading.Interlocked.Increment(ref _nextJobId);
                var job = new CompletedJob
                {
                    JobId = jobId,
                    JobState = 5, // processing - 符合 IPP 规范，表示作业正在处理中
                    JobUri = printerUri.TrimEnd('/') + $"/{jobId}",
                    JobName = jobName ?? "(未指定)",
                    UserName = userName,
                    ClientIp = clientIp,
                    QueueName = queueName,
                    PrinterName = printerName,
                    DocumentFormat = docFormat,
                    DocumentSizeBytes = fileSize,
                    CreatedTime = DateTime.Now,
                    CompletedTime = DateTime.MaxValue, // 使用 MaxValue 表示尚未完成
                    ProcessingHoldUntil = DateTime.Now.AddMilliseconds(ProcessingStateHoldMilliseconds),
                    ErrorMessage = string.Empty
                };
                var payloadPath = TryPersistJobPayload(jobId, ippRequest.DocumentData);
                if (!string.IsNullOrEmpty(payloadPath))
                {
                    job.PayloadPath = payloadPath;
                    job.CanReplay = true;
                }

                var jobList = _completedJobs.GetOrAdd(queueName, _ => new List<CompletedJob>());
                var jobListChanged = false;
                lock (jobList)
                {
                    jobList.Add(job);
                    jobListChanged = true;
                    jobListChanged |= CleanupCompletedJobs(jobList, DateTime.Now);
                    Log($"[{clientIp}] 创建打印作业：job-id={jobId}, 状态=processing, 队列 '{queueName}'");
                }
                if (jobListChanged)
                {
                    PersistJobHistory();
                }
                _jobIdToQueue[jobId] = queueName;
                _jobCancellationTokens[jobId] = timeoutCts;
                ExtendQueueProcessingHold(queueName);
                
                // Print-Job 响应返回 processing (5) 状态，符合 IPP 规范
                // 客户端会通过 Get-Jobs 或 Get-Job-Attributes 查询状态，看到从 processing -> completed 的转换
                var payload = IppResponseWriter.CreatePrintJobResponse(ippRequest.RequestId, ippRequest.Version, jobId, 5, printerUri, jobName, "job-printing");
                response.StatusCode = (int)HttpStatusCode.OK;
                response.ContentType = "application/ipp";
                response.ContentLength64 = payload.Length;
                await response.OutputStream.WriteAsync(payload, 0, payload.Length).ConfigureAwait(false);
                response.OutputStream.Close();
                
                // 诊断日志：记录完整响应内容的十六进制转储
                var hexDump = new StringBuilder();
                for (int i = 0; i < payload.Length; i++)
                {
                    if (i > 0 && i % 16 == 0) hexDump.AppendLine();
                    hexDump.Append($"{payload[i]:X2} ");
                }
                Log($"[{clientIp}] 发送 Print-Job 响应：状态码=0x0000, job-id={jobId}, job-state=processing, 大小={payload.Length} 字节");
                // Log($"[{clientIp}] 响应内容（十六进制）：");
                // Log(hexDump.ToString());
                
                // 异步执行打印（不阻塞响应）
                StartPrintWorker(job, ippRequest.DocumentData, printerName, docName, queueName, clientIp, queueLock, true, timeoutCts, jobList);
                
                // 注意：timeoutCts 和 queueLock 的释放由异步任务负责
                // 不在外层 finally 中释放，因为异步任务可能还在运行
                return;
            }
            catch (OperationCanceledException) when (!token.IsCancellationRequested)
            {
                Log($"[{clientIp}] 打印任务超时：队列 '{queueName}' -> 打印机 '{printerName}'，超过 {PrintJobTimeout.TotalMinutes:F0} 分钟未完成");
                await WriteIppResponseAsync(response, ippRequest.RequestId, ippRequest.Version, 0x0501).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                Log($"[{clientIp}] 打印任务失败：");
                Log($"  文档名：{jobName ?? "(未指定)"}");
                Log($"  打印机：{printerName}");
                Log($"  错误：{ex.Message}");
                // 返回服务器错误
                await WriteIppResponseAsync(response, ippRequest.RequestId, ippRequest.Version, 0x0500).ConfigureAwait(false);
            }
            finally
            {
                // 注意：如果异步任务已启动，timeoutCts 和 queueLock 的释放由异步任务负责
                // 只有在异常情况下（异步任务未启动）才在这里释放
                if (!lockAcquired)
                {
                    timeoutCts?.Dispose();
                }
                // lockAcquired 为 true 时，说明异步任务已启动，由异步任务负责释放
            }
        }
        
        private SemaphoreSlim GetQueueLock(string queueName)
        {
            var key = string.IsNullOrWhiteSpace(queueName) ? string.Empty : queueName;
            return _queueLocks.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
        }
        
        private static string FormatFileSize(long bytes)
        {
            string[] sizes = { "B", "KB", "MB", "GB" };
            double len = bytes;
            int order = 0;
            while (len >= 1024 && order < sizes.Length - 1)
            {
                order++;
                len = len / 1024;
            }
            return $"{len:0.##} {sizes[order]}";
        }
        
        private static string DetectFileFormat(byte[] data, string jobName)
        {
            if (data != null && data.Length >= 4)
            {
                if (data.Length >= 4 && data[0] == 0x25 && data[1] == 0x50 && data[2] == 0x44 && data[3] == 0x46)
                {
                    return "PDF";
                }
                
                if (data.Length >= 3 && data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF)
                {
                    return "JPEG";
                }
                
                if (data.Length >= 8 &&
                    data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47 &&
                    data[4] == 0x0D && data[5] == 0x0A && data[6] == 0x1A && data[7] == 0x0A)
                {
                    return "PNG";
                }
                
                if (data.Length >= 6 &&
                    data[0] == 0x47 && data[1] == 0x49 && data[2] == 0x46 &&
                    data[3] == 0x38 && (data[4] == 0x37 || data[4] == 0x39) && data[5] == 0x61)
                {
                    return "GIF";
                }
                
                if (data.Length >= 2 && data[0] == 0x50 && data[1] == 0x4B)
                {
                    try
                    {
                        var dataStr = Encoding.ASCII.GetString(data, 0, Math.Min(2048, data.Length));
                        if (dataStr.Contains("[Content_Types].xml"))
                        {
                            if (dataStr.Contains("word/") || dataStr.Contains("document.xml"))
                            {
                                return "DOCX";
                            }
                            if (dataStr.Contains("xl/") || dataStr.Contains("worksheets/"))
                            {
                                return "XLSX";
                            }
                            if (dataStr.Contains("ppt/") || dataStr.Contains("slides/"))
                            {
                                return "PPTX";
                            }
                        }
                    }
                    catch
                    {
                        // ignore
                    }
                    return "ZIP";
                }
                
                if (data.Length >= 4 &&
                    ((data[0] == 0x49 && data[1] == 0x49 && data[2] == 0x2A && data[3] == 0x00) ||
                     (data[0] == 0x4D && data[1] == 0x4D && data[2] == 0x00 && data[3] == 0x2A)))
                {
                    return "TIFF";
                }
                
                if (data.Length >= 2 && data[0] == 0x42 && data[1] == 0x4D)
                {
                    return "BMP";
                }
                
                if (data.Length >= 4 && data[0] == 0x25 && data[1] == 0x21 && data[2] == 0x50 && data[3] == 0x53)
                {
                    return "PostScript";
                }
                
                if (data.Length >= 2 && data[0] == 0x1B && data[1] == 0x45)
                {
                    return "PCL";
                }
                
                if (IsPlainText(data))
                {
                    return "TXT";
                }
            }
            
            if (!string.IsNullOrWhiteSpace(jobName))
            {
                var ext = Path.GetExtension(jobName);
                if (!string.IsNullOrWhiteSpace(ext))
                {
                    return ext.TrimStart('.').ToUpperInvariant();
                }
            }
            
            return "二进制";
        }

        private static bool IsPlainText(byte[] data)
        {
            if (data == null || data.Length == 0)
            {
                return false;
            }
            
            bool isText = true;
            int checkLength = Math.Min(512, data.Length);
            for (int i = 0; i < checkLength; i++)
            {
                byte b = data[i];
                if (b < 0x09 || (b > 0x0D && b < 0x20 && b != 0x1B) || b > 0x7E)
                {
                    if (i == 0 && b == 0xEF && data.Length >= 3 && data[1] == 0xBB && data[2] == 0xBF)
                    {
                        i = 2;
                        continue;
                    }
                    isText = false;
                    break;
                }
            }
            return isText;
        }

        private async Task HandleGetPrinterAttributesAsync(HttpListenerRequest rawRequest, HttpListenerResponse response, IppRequest ippRequest, string printerName, string queueName)
        {
            // 构建完整的打印机 URI（包含协议、主机、端口和路径）
            // 注意：Windows 可能使用 ipp:// 协议，但实际传输是 http://
            // 我们返回的 URI 应该与请求中的 printer-uri 格式保持一致
            var requestUri = rawRequest.Url;
            var requestedPrinterUri = ExtractAttributeValue(ippRequest, "printer-uri", Encoding.UTF8);
            
            // 如果请求中指定了 printer-uri，使用客户端请求的 URI（保持客户端期望的地址）
            // 注意：不要替换主机，因为客户端可能通过 NAT 或不同的网络接口访问
            string printerUri;
            if (!string.IsNullOrEmpty(requestedPrinterUri))
            {
                // 直接使用客户端请求的 URI，保持客户端期望的地址和协议
                printerUri = requestedPrinterUri;
            }
            else
            {
                // 如果客户端未指定 printer-uri，使用请求的实际地址
                // 默认使用 http://（因为 HttpListener 使用 HTTP）
                printerUri = $"{requestUri.Scheme}://{requestUri.Host}:{requestUri.Port}{requestUri.AbsolutePath}";
            }
            
            // 提取请求的属性列表（如果指定了 requested-attributes）
            var requestedAttributes = ExtractRequestedAttributes(ippRequest);
            
            // 提取请求的 charset 和 language（用于匹配响应）
            var requestedCharset = ExtractAttributeValueRaw(ippRequest, "attributes-charset", Encoding.ASCII) ?? "utf-8";
            var requestedLanguage = ExtractAttributeValue(ippRequest, "attributes-natural-language") ?? "en-us";

            // 计算当前队列中的活动作业数，用于返回 queued-job-count 和 printer-state
            // 活动作业：pending(3)、processing(5)
            var now = DateTime.Now;
            int activeJobCount = 0;
            int? printerStateOverride = null;
            bool hasProcessingHold = IsQueueInProcessingHold(queueName, now);
            if (!string.IsNullOrEmpty(queueName))
            {
                var jobList = _completedJobs.GetOrAdd(queueName, _ => new List<CompletedJob>());
                var removedByCleanup = false;
                lock (jobList)
                {
                    activeJobCount = jobList.Count(j => j.JobState == 3 || j.JobState == 5);
                    if (!hasProcessingHold)
                    {
                        hasProcessingHold = jobList.Any(j => j.JobState == 5 || j.ProcessingHoldUntil > now);
                    }
                    removedByCleanup = CleanupCompletedJobs(jobList, now);
                }
                if (removedByCleanup)
                {
                    PersistJobHistory();
                }
                // 如果存在活动作业，则将打印机状态设置为 processing(5)，否则保持 idle(3)
                if (activeJobCount > 0 || hasProcessingHold)
                {
                    printerStateOverride = 5;
                }
            }
            
            var payload = IppResponseWriter.CreatePrinterAttributesResponse(
                ippRequest.RequestId,
                ippRequest.Version,
                printerUri,
                printerName,
                Environment.MachineName,
                requestedAttributes,
                requestedCharset,
                requestedLanguage,
                hasProcessingHold ? Math.Max(activeJobCount, 1) : activeJobCount,
                printerStateOverride);
            
            response.StatusCode = (int)HttpStatusCode.OK;
            response.ContentType = "application/ipp";
            response.ContentLength64 = payload.Length;
            
            await response.OutputStream.WriteAsync(payload, 0, payload.Length).ConfigureAwait(false);
            response.OutputStream.Close();
        }

        private static string ExtractAttributeValue(IppRequest ippRequest, string attributeName, Encoding encoding = null)
        {
            foreach (var attr in ippRequest.Attributes)
            {
                if (string.Equals(attr.Name, attributeName, StringComparison.OrdinalIgnoreCase))
                {
                    if (encoding == null)
                    {
                        var charset = ExtractAttributeValueRaw(ippRequest, "attributes-charset", Encoding.ASCII) ?? "utf-8";
                        encoding = charset.Equals("utf-8", StringComparison.OrdinalIgnoreCase)
                            ? Encoding.UTF8
                            : Encoding.GetEncoding(charset);
                    }

                    // text-with-language（0x35）包含 language + value，需要特殊处理
                    if (attr.Tag == 0x35 && attr.RawValue?.Length > 4)
                    {
                        try
                        {
                            var langLen = (attr.RawValue[0] << 8) + attr.RawValue[1];
                            var textLen = (attr.RawValue[2 + langLen] << 8) + attr.RawValue[3 + langLen];
                            var textOffset = 4 + langLen;
                            if (textOffset + textLen <= attr.RawValue.Length)
                            {
                                return encoding.GetString(attr.RawValue, textOffset, textLen);
                            }
                        }
                        catch
                        {
                            // ignore malformed text-with-language
                        }
                    }

                    try
                    {
                        return encoding.GetString(attr.RawValue);
                    }
                    catch
                    {
                        try
                        {
                            return Encoding.UTF8.GetString(attr.RawValue);
                        }
                        catch
                        {
                            return Encoding.ASCII.GetString(attr.RawValue);
                        }
                    }
                }
            }
            return null;
        }
        
        private static string ExtractAttributeValueRaw(IppRequest ippRequest, string attributeName, Encoding encoding)
        {
            foreach (var attr in ippRequest.Attributes)
            {
                if (string.Equals(attr.Name, attributeName, StringComparison.OrdinalIgnoreCase))
                {
                    return encoding.GetString(attr.RawValue);
                }
            }
            return null;
        }

        private async Task HandleCreateJobAsync(HttpListenerResponse response, IppRequest ippRequest, string printerName, string queueName)
        {
            // Create-Job 返回 job-id，Windows 可能使用此方式提交打印任务
            // 简化实现：直接返回成功，job-id = 1
            var payload = IppResponseWriter.CreateCreateJobResponse(ippRequest.RequestId, ippRequest.Version, 1);
            response.StatusCode = (int)HttpStatusCode.OK;
            response.ContentType = "application/ipp";
            response.ContentLength64 = payload.Length;
            await response.OutputStream.WriteAsync(payload, 0, payload.Length).ConfigureAwait(false);
            response.OutputStream.Close();
        }

        private async Task HandleGetJobsAsync(HttpListenerResponse response, IppRequest ippRequest, string printerName, string queueName, string clientIp)
        {
            Log($"[{clientIp}] 收到 Get-Jobs 请求：队列 '{queueName}'");
            
            // 根据 IPP 规范和 Windows 客户端的行为，Get-Jobs 应该返回：
            // 1. 未完成的作业（pending=3, processing=5）
            // 2. 最近完成的作业（completed=9，但只返回最近60秒内完成的，以便客户端能看到最终状态）
            var jobList = _completedJobs.GetOrAdd(queueName, _ => new List<CompletedJob>());
            List<Ipp.JobInfo> activeJobs;
            var removedByCleanup = false;
            lock (jobList)
            {
                var now = DateTime.Now;
                removedByCleanup = CleanupCompletedJobs(jobList, now);
                
                // 返回未完成的作业（pending=3, processing=5）和最近完成的作业（completed=9，短时间内）
                activeJobs = jobList
                    .Where(j => 
                        j.JobState == 3 || j.JobState == 5 || // pending 和 processing
                        (j.JobState == 9 && j.CompletedTime != DateTime.MaxValue &&
                         (now - j.CompletedTime).TotalSeconds <= CompletedJobVisibleSeconds)) // 最近完成的作业
                    .Select(j => new Ipp.JobInfo { JobId = j.JobId, JobState = j.JobState, JobUri = j.JobUri, JobName = j.JobName })
                    .ToList();
                var jobStates = string.Join(", ", activeJobs.Select(j => $"job-id={j.JobId}, state={j.JobState}"));
                Log($"[{clientIp}] Get-Jobs：队列 '{queueName}' 中有 {activeJobs.Count} 个活动作业（{jobStates}），总作业数={jobList.Count}");
            }
            if (removedByCleanup)
            {
                PersistJobHistory();
            }
            
            // Get-Jobs 返回未完成的作业列表
            var payload = IppResponseWriter.CreateGetJobsResponse(ippRequest.RequestId, ippRequest.Version, activeJobs);
            response.StatusCode = (int)HttpStatusCode.OK;
            response.ContentType = "application/ipp";
            response.ContentLength64 = payload.Length;
            await response.OutputStream.WriteAsync(payload, 0, payload.Length).ConfigureAwait(false);
            response.OutputStream.Close();
            
            Log($"[{clientIp}] 发送 Get-Jobs 响应：包含 {activeJobs.Count} 个活动作业");
        }

        private async Task HandleGetJobAttributesAsync(HttpListenerResponse response, IppRequest ippRequest, string printerName, string queueName, string clientIp)
        {
            // 提取 job-id（如果指定）
            var jobIdStr = ExtractAttributeValue(ippRequest, "job-id");
            var jobId = 1; // 默认作业ID
            if (!string.IsNullOrEmpty(jobIdStr) && int.TryParse(jobIdStr, out var parsedJobId))
            {
                jobId = parsedJobId;
            }
            
            Log($"[{clientIp}] 收到 Get-Job-Attributes 请求：队列 '{queueName}', job-id={jobId}");
            
            // 查找作业状态
            var jobList = _completedJobs.GetOrAdd(queueName, _ => new List<CompletedJob>());
            CompletedJob job = null;
            lock (jobList)
            {
                job = jobList.FirstOrDefault(j => j.JobId == jobId);
            }
            
            // 如果找到作业，返回其当前状态；否则返回 completed（兼容旧行为）
            var jobState = job?.JobState ?? 9;
            var jobUri = job?.JobUri;
            var jobName = job?.JobName;
            
            // 记录详细状态信息
            // 根据 IPP 规范：3=pending, 4=pending-held, 5=processing, 6=processing-stopped, 7=canceled, 8=aborted, 9=completed
            var stateName = jobState == 3 ? "pending" : (jobState == 4 ? "pending-held" : (jobState == 5 ? "processing" : (jobState == 6 ? "processing-stopped" : (jobState == 7 ? "canceled" : (jobState == 8 ? "aborted" : (jobState == 9 ? "completed" : "unknown"))))));
            Log($"[{clientIp}] Get-Job-Attributes：job-id={jobId}, job-state={jobState} ({stateName}), job-name={jobName ?? "(未指定)"}");
            
            var payload = IppResponseWriter.CreateGetJobAttributesResponse(ippRequest.RequestId, ippRequest.Version, jobId, jobState, jobUri, jobName);
            response.StatusCode = (int)HttpStatusCode.OK;
            response.ContentType = "application/ipp";
            response.ContentLength64 = payload.Length;
            await response.OutputStream.WriteAsync(payload, 0, payload.Length).ConfigureAwait(false);
            response.OutputStream.Close();
            
            Log($"[{clientIp}] 发送 Get-Job-Attributes 响应：job-id={jobId}, job-state={jobState} ({stateName})");
        }

        private async Task HandleSendDocumentAsync(HttpListenerResponse response, IppRequest ippRequest, string printerName, string queueName, string clientIp, CancellationToken token)
        {
            // Send-Document 是 Create-Job 后的文档发送
            if (ippRequest.DocumentData != null && ippRequest.DocumentData.Length > 0)
            {
                if (ippRequest.DocumentData.Length > MaxJobSizeBytes)
                {
                    var limitStr = FormatFileSize(MaxJobSizeBytes);
                    Log($"[{clientIp}] 文档发送被拒绝：队列 '{queueName}' -> 打印机 '{printerName}'，文件超过 {limitStr}");
                    await WriteIppResponseAsync(response, ippRequest.RequestId, ippRequest.Version, 0x0409).ConfigureAwait(false);
                    return;
                }
                
                var fileSize = ippRequest.DocumentData.Length;
                var fileSizeStr = FormatFileSize(fileSize);
                var startTime = DateTime.Now;
                
                Log($"[{clientIp}] 收到文档数据：队列 '{queueName}' -> 打印机 '{printerName}'，大小：{fileSizeStr}");
                
                var docName = $"IPP Job {queueName} {DateTime.Now:yyyyMMddHHmmss}";
                
                var queueLock = GetQueueLock(queueName);
                var lockAcquired = false;
                CancellationTokenSource timeoutCts = null;
                try
                {
                    await queueLock.WaitAsync(token).ConfigureAwait(false);
                    lockAcquired = true;
                    
                    timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(token);
                    timeoutCts.CancelAfter(PrintJobTimeout);
                    
                    WindowsPrinterDispatcher.SendToPrinter(printerName, ippRequest.DocumentData, docName, timeoutCts.Token);
                    var duration = (DateTime.Now - startTime).TotalSeconds;
                    
                    Log($"[{clientIp}] 文档发送成功：打印机 '{printerName}'，耗时：{duration:F2} 秒");
                }
                catch (OperationCanceledException) when (!token.IsCancellationRequested)
                {
                    Log($"[{clientIp}] 文档发送超时：打印机 '{printerName}'");
                    await WriteIppResponseAsync(response, ippRequest.RequestId, ippRequest.Version, 0x0501).ConfigureAwait(false);
                    return;
                }
                finally
                {
                    timeoutCts?.Dispose();
                    if (lockAcquired)
                    {
                        queueLock.Release();
                    }
                }
                
            }
            await WriteIppResponseAsync(response, ippRequest.RequestId, ippRequest.Version, 0x0000).ConfigureAwait(false);
        }

        private async Task HandleCancelJobAsync(HttpListenerResponse response, IppRequest ippRequest, string queueName, string clientIp)
        {
            var jobIdStr = ExtractAttributeValue(ippRequest, "job-id");
            int jobId = -1;
            if (!string.IsNullOrEmpty(jobIdStr))
            {
                int.TryParse(jobIdStr, out jobId);
            }

            if (jobId <= 0)
            {
                Log($"[{clientIp}] Cancel-Job 请求缺少 job-id 或格式不正确，queue='{queueName}'");
                await WriteIppResponseAsync(response, ippRequest.RequestId, ippRequest.Version, 0x0405).ConfigureAwait(false); // client-error-not-found
                return;
            }

            var jobList = _completedJobs.GetOrAdd(queueName, _ => new List<CompletedJob>());
            bool removed = false;
            int previousState = -1;
            lock (jobList)
            {
                var job = jobList.FirstOrDefault(j => j.JobId == jobId);
                if (job != null)
                {
                    previousState = job.JobState;
                    job.ProcessingHoldUntil = DateTime.Now.AddMilliseconds(ProcessingStateHoldMilliseconds);
                    job.JobState = 7; // canceled
                    job.CompletedTime = DateTime.Now;
                    jobList.Remove(job);
                    removed = true;
                }
            }

            if (removed)
            {
                ExtendQueueProcessingHold(queueName);
                Log($"[{clientIp}] Cancel-Job：已从队列 '{queueName}' 移除 job-id={jobId}，之前状态={previousState}");
                var payload = IppResponseWriter.CreateSimpleOkResponse(ippRequest.RequestId, ippRequest.Version);
                response.StatusCode = (int)HttpStatusCode.OK;
                response.ContentType = "application/ipp";
                response.ContentLength64 = payload.Length;
                await response.OutputStream.WriteAsync(payload, 0, payload.Length).ConfigureAwait(false);
                response.OutputStream.Close();
                return;
            }

            Log($"[{clientIp}] Cancel-Job：未找到 job-id={jobId}，队列 '{queueName}'");
            await WriteIppResponseAsync(response, ippRequest.RequestId, ippRequest.Version, 0x0405).ConfigureAwait(false);
        }

        private static List<string> ExtractRequestedAttributes(IppRequest ippRequest)
        {
            var requested = new List<string>();
            string lastName = null;
            foreach (var attr in ippRequest.Attributes)
            {
                // 检查是否是 requested-attributes 属性
                if (string.Equals(attr.Name, "requested-attributes", StringComparison.OrdinalIgnoreCase))
                {
                    lastName = "requested-attributes";
                    var value = Encoding.ASCII.GetString(attr.RawValue);
                    if (!string.IsNullOrWhiteSpace(value))
                    {
                        if (string.Equals(value, "all", StringComparison.OrdinalIgnoreCase))
                        {
                            return null; // 返回 null 表示返回所有属性
                        }
                        requested.Add(value);
                    }
                }
                // 多值属性的后续值（name 为空，但前一个属性是 requested-attributes）
                else if (string.IsNullOrEmpty(attr.Name) && lastName == "requested-attributes")
                {
                    var value = Encoding.ASCII.GetString(attr.RawValue);
                    if (!string.IsNullOrWhiteSpace(value))
                    {
                        requested.Add(value);
                    }
                }
                else
                {
                    lastName = attr.Name; // 更新最后一个属性名
                }
            }
            return requested.Count > 0 ? requested : null;
        }

        private static async Task<byte[]> ReadBodyAsync(Stream input)
        {
            using (var ms = new MemoryStream())
            {
                await input.CopyToAsync(ms).ConfigureAwait(false);
                return ms.ToArray();
            }
        }

        private static async Task WritePlainResponse(HttpListenerResponse response, HttpStatusCode statusCode, string message)
        {
            response.StatusCode = (int)statusCode;
            var buffer = System.Text.Encoding.UTF8.GetBytes(message ?? string.Empty);
            response.ContentType = "text/plain; charset=utf-8";
            response.ContentLength64 = buffer.Length;
            await response.OutputStream.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
        }

        private async Task HandleWebTestPrintAsync(HttpListenerRequest request, HttpListenerResponse response, string printerName, string queueName, CancellationToken token)
        {
            try
            {
                Log($"Web 测试打印：打印机 '{printerName}'（队列 {queueName}）");
                PrintSystemTestPage(printerName);
                var message = $"已调用系统测试页命令，正在向 {printerName} 发送测试页。";
                
                // 检查是否是AJAX请求（通过Accept头或format参数）
                var acceptHeader = request.Headers["Accept"] ?? string.Empty;
                var format = request.QueryString["format"];
                var isJsonRequest = acceptHeader.Contains("application/json") || 
                                   string.Equals(format, "json", StringComparison.OrdinalIgnoreCase);
                
                if (isJsonRequest)
                {
                    var json = $"{{\"success\":true,\"message\":\"{WebUtility.HtmlEncode(message).Replace("\"", "\\\"")}\"}}";
                    var buffer = Encoding.UTF8.GetBytes(json);
                    response.StatusCode = (int)HttpStatusCode.OK;
                    response.ContentType = "application/json; charset=utf-8";
                    response.ContentLength64 = buffer.Length;
                    await response.OutputStream.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
                }
                else
                {
                    await WriteStatusHtmlResponseAsync(response, $"已调用系统测试页命令，正在向 <strong>{WebUtility.HtmlEncode(printerName)}</strong> 发送测试页。").ConfigureAwait(false);
                }
            }
            catch (Exception ex)
            {
                Log($"Web 测试打印失败：{ex.Message}");
                var errorMessage = $"测试打印失败：{ex.Message}";
                
                // 检查是否是AJAX请求
                var acceptHeader = request.Headers["Accept"] ?? string.Empty;
                var format = request.QueryString["format"];
                var isJsonRequest = acceptHeader.Contains("application/json") || 
                                   string.Equals(format, "json", StringComparison.OrdinalIgnoreCase);
                
                if (isJsonRequest)
                {
                    var json = $"{{\"success\":false,\"message\":\"{WebUtility.HtmlEncode(errorMessage).Replace("\"", "\\\"")}\"}}";
                    var buffer = Encoding.UTF8.GetBytes(json);
                    response.StatusCode = (int)HttpStatusCode.OK;
                    response.ContentType = "application/json; charset=utf-8";
                    response.ContentLength64 = buffer.Length;
                    await response.OutputStream.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
                }
                else
                {
                    await WriteStatusHtmlResponseAsync(response, $"测试打印失败：{WebUtility.HtmlEncode(ex.Message)}").ConfigureAwait(false);
                }
            }
        }

        private async Task WriteStatusHtmlResponseAsync(HttpListenerResponse response, string message)
        {
            var html = "<!DOCTYPE html><html lang=\"zh-cn\"><head><meta charset=\"utf-8\"/><title>测试打印结果</title></head><body>"
                       + $"<p>{message}</p>"
                       + "<p><a href=\"./\">返回状态页面</a></p>"
                       + "</body></html>";
            var buffer = Encoding.UTF8.GetBytes(html);
            response.StatusCode = (int)HttpStatusCode.OK;
            response.ContentType = "text/html; charset=utf-8";
            response.ContentLength64 = buffer.Length;
            await response.OutputStream.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
        }

        private void PrintSystemTestPage(string printerName)
        {
            var psi = new ProcessStartInfo
            {
                FileName = "rundll32.exe",
                Arguments = $"printui.dll,PrintUIEntry /k /n \"{printerName}\"",
                CreateNoWindow = true,
                UseShellExecute = false
            };
            using (var process = Process.Start(psi))
            {
                process?.WaitForExit(10000);
            }
        }

        private async Task WriteStatusPageAsync(HttpListenerResponse response)
        {
            var html = BuildStatusHtml();
            var buffer = Encoding.UTF8.GetBytes(html);
            response.StatusCode = (int)HttpStatusCode.OK;
            response.ContentType = "text/html; charset=utf-8";
            response.ContentLength64 = buffer.Length;
            await response.OutputStream.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
        }

        private async Task WriteAccessDeniedPageAsync(HttpListenerResponse response, string denyReason, string clientIp)
        {
            var html = $@"<!DOCTYPE html>
<html lang=""zh-CN"">
<head>
    <meta charset=""utf-8"">
    <meta http-equiv=""X-UA-Compatible"" content=""IE=edge"">
    <title>访问被拒绝</title>
    <style>
        body {{
            font-family: 'Microsoft YaHei', 'Segoe UI', Arial, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            margin: 0;
            padding: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
        }}
        .error-container {{
            background: white;
            border-radius: 10px;
            box-shadow: 0 10px 40px rgba(0,0,0,0.2);
            padding: 40px;
            max-width: 500px;
            text-align: center;
        }}
        .error-icon {{
            font-size: 64px;
            color: #dc3545;
            margin-bottom: 20px;
        }}
        h1 {{
            color: #333;
            margin: 0 0 20px 0;
            font-size: 24px;
        }}
        .error-message {{
            color: #666;
            line-height: 1.6;
            margin-bottom: 30px;
            font-size: 16px;
        }}
        .error-details {{
            background: #f8f9fa;
            border-left: 4px solid #dc3545;
            padding: 15px;
            margin: 20px 0;
            text-align: left;
            border-radius: 4px;
        }}
        .error-details strong {{
            color: #dc3545;
        }}
    </style>
</head>
<body>
    <div class=""error-container"">
        <div class=""error-icon"">⚠️</div>
        <h1>访问被拒绝</h1>
        <div class=""error-message"">
            <p>您的IP地址或MAC地址已被限制访问此打印服务。</p>
            <div class=""error-details"">
                <strong>拒绝原因：</strong>{System.Net.WebUtility.HtmlEncode(denyReason)}<br>
                <strong>您的IP地址：</strong>{System.Net.WebUtility.HtmlEncode(clientIp)}<br>
                <strong>解决方案：</strong>请联系系统管理员，将您的IP地址或MAC地址添加到允许列表中。
            </div>
        </div>
    </div>
</body>
</html>";
            
            var htmlBytes = System.Text.Encoding.UTF8.GetBytes(html);
            response.StatusCode = (int)HttpStatusCode.Forbidden;
            response.ContentType = "text/html; charset=utf-8";
            response.ContentLength64 = htmlBytes.Length;
            await response.OutputStream.WriteAsync(htmlBytes, 0, htmlBytes.Length).ConfigureAwait(false);
            response.OutputStream.Close();
        }

        private string BuildStatusHtml()
        {
            // 判断是否监听所有网卡
            string basePrefix;
            lock (_configLock)
            {
                basePrefix = _config.BasePrefix;
            }
            var isListenAll = basePrefix.Contains("://+") || basePrefix.Contains("://*") || basePrefix.Contains("://0.0.0.0");
            var localIps = GetLocalIpAddresses() ?? new List<string>();
            // 再次去重，防止上层调用重复
            localIps = localIps.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
            var ippRunning = IsIppServiceEnabled;
            var port = ExtractPortFromPrefix(basePrefix);
            var path = ExtractPathFromPrefix(basePrefix);
            
            var sb = new StringBuilder();
            sb.Append("<!DOCTYPE html><html lang=\"zh-cn\"><head><meta charset=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/>");
            sb.Append("<title>IPP Server 状态</title>");
            sb.Append("<style>");
            sb.Append("*{box-sizing:border-box;margin:0;padding:0;}");
            sb.Append("body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);min-height:100vh;padding:20px;color:#333;}");
            sb.Append(".container{max-width:1200px;margin:0 auto;background:#fff;border-radius:12px;box-shadow:0 10px 40px rgba(0,0,0,0.2);overflow:hidden;}");
            sb.Append(".header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;padding:30px;position:relative;}");
            sb.Append(".header h1{font-size:28px;font-weight:600;margin-bottom:10px;}");
            sb.Append(".admin-link{position:absolute;top:30px;right:30px;background:rgba(255,255,255,0.2);color:#fff;text-decoration:none;padding:10px 20px;border-radius:6px;font-weight:500;transition:all 0.3s;border:1px solid rgba(255,255,255,0.3);}");
            sb.Append(".admin-link:hover{background:rgba(255,255,255,0.3);transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,0.2);}");
            sb.Append(".content{padding:30px;}");
            sb.Append(".section{margin-bottom:30px;background:#f8f9fa;border-radius:8px;padding:20px;border:1px solid #e9ecef;}");
            sb.Append(".section h2{font-size:20px;color:#495057;margin-bottom:15px;padding-bottom:10px;border-bottom:2px solid #dee2e6;}");
            sb.Append(".address-list{display:block;margin-top:10px;}");
            sb.Append(".address-item{display:block;background:#e7f3ff;border:1px solid #b3d9ff;padding:8px 12px;border-radius:6px;font-family:'Courier New',monospace;font-size:13px;color:#0066cc;margin-bottom:6px;}");
            sb.Append("table{width:100%;border-collapse:collapse;background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.05);}");
            sb.Append("th{background:#667eea;color:#fff;padding:12px;text-align:left;font-weight:600;font-size:14px;}");
            sb.Append("td{padding:12px;border-bottom:1px solid #e9ecef;font-size:14px;}");
            sb.Append("tr:hover{background:#f8f9fa;}");
            sb.Append("code{background:#e7f3ff;color:#0066cc;padding:4px 8px;border-radius:4px;font-family:'Courier New',monospace;font-size:13px;border:1px solid #b3d9ff;}");
            sb.Append("ul{list-style:none;padding:0;}");
            sb.Append("ul li{background:#fff;padding:10px 15px;margin-bottom:8px;border-radius:6px;border-left:3px solid #667eea;box-shadow:0 2px 4px rgba(0,0,0,0.05);}");
            sb.Append("ol{padding-left:20px;}");
            sb.Append("ol li{margin-bottom:10px;line-height:1.6;}");
            sb.Append(".btn{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;border:none;padding:8px 16px;border-radius:6px;cursor:pointer;font-size:14px;font-weight:500;transition:all 0.3s;box-shadow:0 2px 4px rgba(102,126,234,0.3);}");
            sb.Append(".btn:hover{transform:translateY(-2px);box-shadow:0 4px 8px rgba(102,126,234,0.4);}");
            sb.Append(".btn:active{transform:translateY(0);}");
            sb.Append(".btn:disabled{background:#ccc;cursor:not-allowed;transform:none;box-shadow:none;}");
            sb.Append(".btn-copy{background:#28a745;margin-left:8px;}");
            sb.Append(".btn-copy:hover{background:#218838;box-shadow:0 4px 8px rgba(40,167,69,0.4);}");
            sb.Append(".btn-group{display:flex;align-items:center;gap:8px;}");
            sb.Append(".test-msg{margin-top:10px;padding:10px;border-radius:6px;display:none;font-size:13px;}");
            sb.Append(".test-msg.success{background:#d4edda;color:#155724;border:1px solid #c3e6cb;}");
            sb.Append(".test-msg.error{background:#f8d7da;color:#721c24;border:1px solid #f5c6cb;}");
            sb.Append(".status-badge{display:inline-block;min-width:52px;padding:2px 10px;border-radius:999px;font-size:12px;font-weight:500;text-align:center;margin-right:8px;}");
            sb.Append(".status-badge.on{background:#d4edda;color:#155724;border:1px solid #c3e6cb;}");
            sb.Append(".status-badge.off{background:#f8d7da;color:#721c24;border:1px solid #f5c6cb;}");
            sb.Append(".empty-state{text-align:center;padding:40px;color:#6c757d;}");
            sb.Append(".empty-state p{font-size:16px;}");
            sb.Append("footer{text-align:center;padding:20px;color:#6c757d;border-top:1px solid #e9ecef;margin-top:30px;}");
            sb.Append("@media (max-width:768px){.header{padding:20px;}.header h1{font-size:24px;}.admin-link{position:static;display:inline-block;margin-top:10px;}.content{padding:20px;}.address-list{flex-direction:column;}table{font-size:12px;}th,td{padding:8px;}}</style>");
            sb.Append("<script>function testPrint(queueName,printerName){var btn=event.target;var originalText=btn.textContent;btn.disabled=true;btn.textContent='测试中...';var msgDiv=document.getElementById('test-msg-'+queueName);if(msgDiv){msgDiv.style.display='none';}var url='/printers/'+encodeURIComponent(queueName)+'?test=1&format=json';fetch(url,{method:'GET',headers:{'Accept':'application/json'}}).then(function(response){return response.json();}).then(function(data){btn.disabled=false;btn.textContent=originalText;if(msgDiv){msgDiv.className='test-msg '+(data.success?'success':'error');msgDiv.textContent=data.message;msgDiv.style.display='block';}if(data.success){setTimeout(function(){if(msgDiv){msgDiv.style.display='none';}},5000);}}).catch(function(error){btn.disabled=false;btn.textContent=originalText;if(msgDiv){msgDiv.className='test-msg error';msgDiv.textContent='测试打印失败：'+error.message;msgDiv.style.display='block';}});}function copyUri(uri,queueName){var btn=event.target;var originalText=btn.textContent;btn.disabled=true;btn.textContent='复制中...';var msgDiv=document.getElementById('test-msg-'+queueName);if(msgDiv){msgDiv.style.display='none';}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(uri).then(function(){btn.disabled=false;btn.textContent=originalText;if(msgDiv){msgDiv.className='test-msg success';msgDiv.textContent='地址已复制到剪贴板';msgDiv.style.display='block';setTimeout(function(){if(msgDiv){msgDiv.style.display='none';}},3000);}}).catch(function(err){fallbackCopy(uri,btn,originalText,msgDiv);});}else{fallbackCopy(uri,btn,originalText,msgDiv);}}function fallbackCopy(text,btn,originalText,msgDiv){var textArea=document.createElement('textarea');textArea.value=text;textArea.style.position='fixed';textArea.style.left='-999999px';textArea.style.top='-999999px';document.body.appendChild(textArea);textArea.focus();textArea.select();try{var successful=document.execCommand('copy');btn.disabled=false;btn.textContent=originalText;if(successful){if(msgDiv){msgDiv.className='test-msg success';msgDiv.textContent='地址已复制到剪贴板';msgDiv.style.display='block';setTimeout(function(){if(msgDiv){msgDiv.style.display='none';}},3000);}}else{if(msgDiv){msgDiv.className='test-msg error';msgDiv.textContent='复制失败，请手动复制地址';msgDiv.style.display='block';setTimeout(function(){if(msgDiv){msgDiv.style.display='none';}},3000);}}}catch(err){btn.disabled=false;btn.textContent=originalText;if(msgDiv){msgDiv.className='test-msg error';msgDiv.textContent='复制失败：'+err.message;msgDiv.style.display='block';setTimeout(function(){if(msgDiv){msgDiv.style.display='none';}},3000);}}document.body.removeChild(textArea);}</script>");
            sb.Append("</head><body>");
            sb.Append("<div class=\"container\">");
            sb.Append("<div class=\"header\">");
            sb.Append("<h1>IPP Server 状态</h1>");
            var adminPath = path.TrimEnd('/') + "/admin";
            sb.AppendFormat("<a href=\"{0}\" class=\"admin-link\">进入管理界面 →</a>", WebUtility.HtmlEncode(adminPath));
            sb.Append("</div>");
            sb.Append("<div class=\"content\">");
            
            // 监听地址部分
            sb.Append("<div class=\"section\">");
            sb.Append("<h2>监听地址</h2>");
            if (isListenAll && localIps.Count > 0)
            {
                sb.Append("<p style=\"margin-bottom:10px;color:#6c757d;\">当前监听所有网卡，客户端根据网段可使用以下同网段地址访问状态页面：</p>");
                sb.Append("<div class=\"address-list\">");
                foreach (var ip in localIps)
                {
                    var httpUrl = $"http://{ip}:{port}{path}";
                    var ippUrl = $"ipp://{ip}:{port}{path}";
                    sb.AppendFormat("<div class=\"address-item\">{0}</div>", WebUtility.HtmlEncode(httpUrl));
                    sb.AppendFormat("<div class=\"address-item\">{0}</div>", WebUtility.HtmlEncode(ippUrl));
                }
                sb.Append("</div>");
            }
            else
            {
                var displayPrefix = GetDisplayBasePrefix() ?? basePrefix;
                sb.AppendFormat("<p style=\"margin-bottom:10px;color:#6c757d;\">监听地址：</p>");
                sb.AppendFormat("<div class=\"address-list\"><div class=\"address-item\">{0}</div></div>", WebUtility.HtmlEncode(displayPrefix));
            }
            sb.Append("</div>");

            // 已共享的打印机部分
            sb.Append("<div class=\"section\">");
            sb.Append("<h2>已共享的打印机</h2>");
            if (_config.QueueToPrinter.Count == 0)
            {
                sb.Append("<div class=\"empty-state\"><p>当前尚未配置任何队列。请在管理控制台中添加本地打印机。</p></div>");
            }
            else
            {
                sb.Append("<table><thead><tr><th>队列路径</th><th>目标打印机</th><th>客户端 URI 示例</th><th>操作</th></tr></thead><tbody>");
                // 每个队列的 URI 显示策略：
                // - 如果监听所有网卡且有多个 IP：为每个 IP 单独列一行队列 URI；
                // - 否则（只监听一个前缀/单 IP 等）：每个队列只显示一条 URI 示例。
                foreach (var kvp in _config.QueueToPrinter)
                {
                    var queueRaw = kvp.Key;
                    var printerRaw = kvp.Value;
                    var queue = WebUtility.HtmlEncode(queueRaw);
                    var printer = WebUtility.HtmlEncode(printerRaw);
                    var safeQueue = queueRaw.Replace("'", "\\'").Replace("\"", "&quot;");
                    var safePrinter = printerRaw.Replace("'", "\\'").Replace("\"", "&quot;");
                    var statusClass = ippRunning ? "on" : "off";
                    var statusText = ippRunning ? "已启动" : "未启动";

                    if (isListenAll && localIps.Count > 1)
                    {
                        // 监听所有网卡且有多个 IP：按 IP 展开多行
                        foreach (var ip in localIps)
                        {
                            if (string.IsNullOrWhiteSpace(ip))
                            {
                                continue;
                            }

                            var uri = $"http://{ip}:{port}{path}{queueRaw}";
                            var uriDisplay = WebUtility.HtmlEncode(uri);
                            var safeSample = uri.Replace("'", "\\'").Replace("\"", "&quot;");

                            sb.AppendFormat(
                                "<tr><td><code>{0}</code></td><td>{1}</td><td><code>{2}</code></td><td><div class=\"btn-group\"><span class=\"status-badge {3}\">{4}</span><button class=\"btn\" onclick=\"testPrint('{5}','{6}')\">测试打印</button><button class=\"btn btn-copy\" onclick=\"copyUri('{7}','{5}')\">复制地址</button></div><div id=\"test-msg-{8}\" class=\"test-msg\"></div></td></tr>",
                                queue, printer, uriDisplay, statusClass, statusText, safeQueue, safePrinter, safeSample, queue);
                        }
                    }
                    else
                    {
                        // 非“监听所有网卡”场景：每个队列只显示一条示例 URI
                        var sampleBase = GetDisplayBasePrefix();
                        if (string.IsNullOrWhiteSpace(sampleBase))
                        {
                            sampleBase = NormalizeBasePrefix(_config.BasePrefix ?? string.Empty);
                        }
                        if (!string.IsNullOrWhiteSpace(sampleBase) && sampleBase.EndsWith("/", StringComparison.Ordinal))
                        {
                            sampleBase = sampleBase.TrimEnd('/');
                        }

                        var sample = string.IsNullOrWhiteSpace(sampleBase)
                            ? BuildPrinterUri(queueRaw)
                            : $"{sampleBase}/{WebUtility.HtmlEncode(queueRaw)}";
                        var safeSample = sample.Replace("'", "\\'").Replace("\"", "&quot;");

                        sb.AppendFormat(
                            "<tr><td><code>{0}</code></td><td>{1}</td><td><code>{2}</code></td><td><div class=\"btn-group\"><span class=\"status-badge {3}\">{4}</span><button class=\"btn\" onclick=\"testPrint('{5}','{6}')\">测试打印</button><button class=\"btn btn-copy\" onclick=\"copyUri('{7}','{5}')\">复制地址</button></div><div id=\"test-msg-{8}\" class=\"test-msg\"></div></td></tr>",
                            queue, printer, sample, statusClass, statusText, safeQueue, safePrinter, safeSample, queue);
                    }
                }
                sb.Append("</tbody></table>");
            }
            sb.Append("</div>");

            // 本机已安装打印机部分
            sb.Append("<div class=\"section\">");
            sb.Append("<h2>本机已安装打印机</h2>");
            var physicalPrinters = PrinterFilter.GetPhysicalPrinters();
            if (physicalPrinters.Count == 0)
            {
                sb.Append("<div class=\"empty-state\"><p>未检测到任何物理打印机。</p></div>");
            }
            else
            {
                sb.Append("<ul>");
                foreach (string printer in physicalPrinters)
                {
                    sb.AppendFormat("<li>{0}</li>", WebUtility.HtmlEncode(printer));
                }
                sb.Append("</ul>");
            }
            sb.Append("</div>");

            // 客户端连接提示部分
            sb.Append("<div class=\"section\">");
            sb.Append("<h2>客户端连接提示</h2>");
            sb.Append("<ol>");
            sb.Append("<li>在 Windows <strong>\"添加打印机\"</strong>向导中选择 <strong>\"我所需要的打印机未列出\"</strong>。</li>");
            var exampleBase = GetDisplayBasePrefix();
            if (string.IsNullOrWhiteSpace(exampleBase))
            {
                exampleBase = NormalizeBasePrefix(_config.BasePrefix ?? string.Empty);
            }
            if (string.IsNullOrWhiteSpace(exampleBase))
            {
                var exampleIp = localIps.FirstOrDefault(ip => !ip.StartsWith("169.254.", StringComparison.Ordinal)) ?? localIps.FirstOrDefault() ?? "服务器IP";
                exampleBase = $"http://{exampleIp}:{port}{path}";
            }
            if (exampleBase.EndsWith("/", StringComparison.Ordinal))
            {
                exampleBase = exampleBase.TrimEnd('/');
            }
            var exampleUri = $"{exampleBase}/队列名称;";
            sb.AppendFormat("<li>然后选择<strong>\"按名称选择共享打印机\"</strong>，在编辑框中输入 URI：<code>{0}</code>，点击下一步完成IPP打印机安装输入。</li>", WebUtility.HtmlEncode(exampleUri));
            sb.Append("<li>客户端需安装对应打印机驱动，与服务端保持一致。</li>");
            sb.Append("</ol>");
            // 下载客户端辅助工具链接
            sb.Append("<div style=\"margin-top:15px;padding:12px 16px;background:#e3f2fd;border-left:4px solid #2196f3;border-radius:6px;font-size:14px;color:#555;\">");
            sb.Append("<strong>可选：</strong>也可以在客户端上安装并运行 <strong>客户端辅助工具</strong>，自动发现并添加打印机。");
            sb.Append("<a href=\"clienttools/ipp-printer-install-helper.exe\" style=\"margin-left:10px;padding:6px 12px;background:#007bff;color:#fff;border-radius:4px;text-decoration:none;font-weight:500;\">下载客户端辅助工具</a>");
            sb.Append("</div>");
            sb.Append("</div>");

            sb.Append("<footer><small>IPP Server &copy; ")
              .Append(DateTime.Now.Year)
              .Append("</small></footer>");
            sb.Append("</div></div>");
            sb.Append("</body></html>");
            return sb.ToString();
        }

        private byte[] LoadClientToolBinary()
        {
            if (_clientToolBinary != null && _clientToolBinary.Length > 0)
            {
                return _clientToolBinary;
            }

            lock (_clientToolLock)
            {
                if (_clientToolBinary != null && _clientToolBinary.Length > 0)
                {
                    return _clientToolBinary;
                }

                try
                {
                    var assembly = Assembly.GetExecutingAssembly();
                    // 资源命名可能包含命名空间路径，尝试根据后缀匹配
                    var resourceName = assembly.GetManifestResourceNames()
                        .FirstOrDefault(n => n.EndsWith("ClientTools.IppPrinterInstallHelper.exe", StringComparison.OrdinalIgnoreCase));

                    if (resourceName == null)
                    {
                        Log("未找到嵌入的客户端辅助工具资源（ClientTools.IppPrinterInstallHelper.exe）。");
                        return null;
                    }

                    using (var stream = assembly.GetManifestResourceStream(resourceName))
                    {
                        if (stream == null)
                        {
                            Log($"无法读取客户端辅助工具资源：{resourceName}");
                            return null;
                        }

                        using (var ms = new MemoryStream())
                        {
                            stream.CopyTo(ms);
                            _clientToolBinary = ms.ToArray();
                        }
                    }

                    Log("客户端辅助工具资源已加载，可通过 WebUI 下载。");
                    return _clientToolBinary;
                }
                catch (Exception ex)
                {
                    Log($"加载客户端辅助工具资源失败：{ex.Message}");
                    return null;
                }
            }
        }

        private static async Task WriteIppResponseAsync(HttpListenerResponse response, int requestId, ushort version, ushort statusCode)
        {
            var payload = statusCode == 0x0000
                ? IppResponseWriter.CreateSimpleOkResponse(requestId, version)
                : IppResponseWriter.CreateErrorResponse(requestId, statusCode, version);
            response.StatusCode = (int)HttpStatusCode.OK;
            response.ContentType = "application/ipp";
            response.ContentLength64 = payload.Length;
            await response.OutputStream.WriteAsync(payload, 0, payload.Length).ConfigureAwait(false);
            response.OutputStream.Close();
        }

        private void ExtendQueueProcessingHold(string queueName)
        {
            if (string.IsNullOrEmpty(queueName))
            {
                return;
            }
            var holdUntil = DateTime.Now.AddMilliseconds(ProcessingStateHoldMilliseconds);
            _queueProcessingHold.AddOrUpdate(queueName, holdUntil, (_, existing) => existing > holdUntil ? existing : holdUntil);
        }

        private bool IsQueueInProcessingHold(string queueName, DateTime now)
        {
            if (string.IsNullOrEmpty(queueName))
            {
                return false;
            }
            if (_queueProcessingHold.TryGetValue(queueName, out var holdUntil))
            {
                if (holdUntil > now)
                {
                    return true;
                }
                _queueProcessingHold.TryRemove(queueName, out _);
            }
            return false;
        }

        private string TryPersistJobPayload(int jobId, byte[] data)
        {
            if (data == null || data.Length == 0)
            {
                return null;
            }
            if (data.LongLength > MaxReplayJobSizeBytes)
            {
                return null;
            }
            try
            {
                Directory.CreateDirectory(_jobCacheDirectory);
                var fileName = $"{DateTime.UtcNow:yyyyMMddHHmmssfff}_{jobId}.ippjob";
                var path = Path.Combine(_jobCacheDirectory, fileName);
                File.WriteAllBytes(path, data);
                return path;
            }
            catch (Exception ex)
            {
                Log($"保存作业缓存失败：job-id={jobId}, error={ex.Message}");
                return null;
            }
        }

        private void CleanupJobResources(CompletedJob job)
        {
            if (job == null)
            {
                return;
            }

            _jobIdToQueue.TryRemove(job.JobId, out _);

            if (!string.IsNullOrEmpty(job.PayloadPath))
            {
                try
                {
                    if (File.Exists(job.PayloadPath))
                    {
                        File.Delete(job.PayloadPath);
                    }
                }
                catch (Exception ex)
                {
                    Log($"删除作业缓存失败：job-id={job.JobId}, error={ex.Message}");
                }
            }
        }

        private bool TryGetJobList(int jobId, out string queueName, out List<CompletedJob> jobList)
        {
            jobList = null;
            queueName = null;
            
            if (!_jobIdToQueue.TryGetValue(jobId, out queueName))
            {
                return false;
            }
            
            if (!_completedJobs.TryGetValue(queueName, out jobList))
            {
                return false;
            }
            
            return true;
        }

        private (bool Success, string Message) StopJobInternal(int jobId, bool deleteRecord)
        {
            if (!TryGetJobList(jobId, out var queueName, out var jobList))
            {
                return (false, "未找到指定作业");
            }

            CompletedJob targetJob = null;
            var jobListChanged = false;
            lock (jobList)
            {
                targetJob = jobList.FirstOrDefault(j => j.JobId == jobId);
                if (targetJob == null)
                {
                    return (false, "作业记录已不存在");
                }

                if (!deleteRecord && targetJob.JobState == 9)
                {
                    return (false, "作业已完成，无法停止");
                }

                if (deleteRecord)
                {
                    jobList.Remove(targetJob);
                    _jobIdToQueue.TryRemove(jobId, out _);
                    jobListChanged = true;
                }
                else
                {
                    targetJob.JobState = 7; // canceled
                    targetJob.CompletedTime = DateTime.Now;
                    targetJob.ErrorMessage = "管理员手动停止作业";
                    targetJob.ProcessingHoldUntil = DateTime.Now.AddSeconds(CompletedJobVisibleSeconds);
                    jobListChanged = true;
                }
            }

            if (jobListChanged)
            {
                PersistJobHistory();
            }

            if (_jobCancellationTokens.TryGetValue(jobId, out var cts))
            {
                cts.Cancel();
            }

            if (deleteRecord)
            {
                CleanupJobResources(targetJob);
                return (true, "已删除作业记录");
            }

            return (true, "已发送停止指令");
        }

        private async Task<(bool Success, string Message)> StopJobAsync(int jobId)
        {
            return await Task.FromResult(StopJobInternal(jobId, deleteRecord: false)).ConfigureAwait(false);
        }

        private async Task<(bool Success, string Message)> DeleteJobAsync(int jobId)
        {
            return await Task.FromResult(StopJobInternal(jobId, deleteRecord: true)).ConfigureAwait(false);
        }

        private async Task<(bool Success, string Message)> ReplayJobAsync(int jobId)
        {
            if (!TryGetJobList(jobId, out var queueName, out var jobList))
            {
                return (false, "未找到指定作业");
            }

            CompletedJob targetJob;
            lock (jobList)
            {
                targetJob = jobList.FirstOrDefault(j => j.JobId == jobId);
                if (targetJob == null)
                {
                    return (false, "作业记录已不存在");
                }
            }

            if (!targetJob.CanReplay || string.IsNullOrEmpty(targetJob.PayloadPath) || !File.Exists(targetJob.PayloadPath))
            {
                return (false, "该作业不支持重打或缓存已过期");
            }

            byte[] payload;
            try
            {
                payload = await Task.Run(() => File.ReadAllBytes(targetJob.PayloadPath)).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                return (false, $"读取作业缓存失败：{ex.Message}");
            }

            var replayJobId = await StartManualPrintJobAsync(
                queueName,
                targetJob.PrinterName,
                payload,
                targetJob.JobName ?? "(未指定)",
                targetJob.UserName ?? "管理员重打",
                targetJob.DocumentFormat,
                targetJob.PayloadPath,
                $"{targetJob.ClientIp} / Web").ConfigureAwait(false);

            return (true, $"已提交重打任务（新作业ID：{replayJobId}）");
        }

        private async Task<int> StartManualPrintJobAsync(
            string queueName,
            string printerName,
            byte[] documentData,
            string originalJobName,
            string userName,
            string documentFormat,
            string payloadPath,
            string clientIp)
        {
            if (documentData == null || documentData.Length == 0)
            {
                throw new InvalidOperationException("重打数据为空，无法提交打印任务");
            }

            var queueLock = GetQueueLock(queueName);
            var lockAcquired = false;
            CancellationTokenSource timeoutCts = null;

            try
            {
                await queueLock.WaitAsync(_cts.Token).ConfigureAwait(false);
                lockAcquired = true;

                timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token);
                timeoutCts.CancelAfter(PrintJobTimeout);

                var jobId = Interlocked.Increment(ref _nextJobId);
                var printerUri = BuildPrinterUri(queueName);
                var job = new CompletedJob
                {
                    JobId = jobId,
                    JobState = 5,
                    JobUri = printerUri.TrimEnd('/') + $"/{jobId}",
                    JobName = $"{originalJobName ?? "(未指定)"} (重打)",
                    UserName = $"{userName ?? "Web管理员"} (重打)",
                    ClientIp = clientIp ?? "WebAdmin",
                    QueueName = queueName,
                    PrinterName = printerName,
                    DocumentFormat = documentFormat,
                    DocumentSizeBytes = documentData.LongLength,
                    CreatedTime = DateTime.Now,
                    CompletedTime = DateTime.MaxValue,
                    ProcessingHoldUntil = DateTime.Now.AddMilliseconds(ProcessingStateHoldMilliseconds),
                    PayloadPath = payloadPath,
                    CanReplay = !string.IsNullOrEmpty(payloadPath) && File.Exists(payloadPath),
                    ErrorMessage = string.Empty
                };

                var jobList = _completedJobs.GetOrAdd(queueName, _ => new List<CompletedJob>());
                var jobListChanged = false;
                lock (jobList)
                {
                    jobList.Add(job);
                    jobListChanged = true;
                    jobListChanged |= CleanupCompletedJobs(jobList, DateTime.Now);
                }
                if (jobListChanged)
                {
                    PersistJobHistory();
                }

                _jobIdToQueue[jobId] = queueName;
                _jobCancellationTokens[jobId] = timeoutCts;

                var docName = $"ADMIN-REPLAY-{DateTime.Now:yyyyMMddHHmmss}";
                StartPrintWorker(job, documentData, printerName, docName, queueName, job.ClientIp, queueLock, true, timeoutCts, jobList);
                return jobId;
            }
            catch
            {
                timeoutCts?.Dispose();
                if (lockAcquired)
                {
                    queueLock.Release();
                }
                throw;
            }
        }

        private void StartPrintWorker(
            CompletedJob job,
            byte[] documentData,
            string printerName,
            string docName,
            string queueName,
            string clientIp,
            SemaphoreSlim queueLock,
            bool releaseLock,
            CancellationTokenSource timeoutCts,
            List<CompletedJob> jobList)
        {
            _ = Task.Run(async () =>
            {
                try
                {
                    var printStartTime = DateTime.Now;
                    WindowsPrinterDispatcher.SendToPrinter(printerName, documentData, docName, timeoutCts.Token);
                    var duration = (DateTime.Now - printStartTime).TotalSeconds;

                    try
                    {
                        await Task.Delay(ProcessingStateHoldMilliseconds, timeoutCts.Token).ConfigureAwait(false);
                    }
                    catch (TaskCanceledException)
                    {
                    }

                    var stateUpdated = false;
                    lock (jobList)
                    {
                        if (job.JobState == 5)
                        {
                            job.JobState = 9;
                            job.CompletedTime = DateTime.Now;
                            job.ProcessingHoldUntil = DateTime.Now.AddMilliseconds(ProcessingStateHoldMilliseconds);
                            stateUpdated = true;
                        }
                    }
                    if (stateUpdated)
                    {
                        PersistJobHistory();
                    }
                    ExtendQueueProcessingHold(queueName);

                    await Task.Delay(TimeSpan.FromSeconds(CompletedJobVisibleSeconds)).ConfigureAwait(false);
                    var removedByCleanup = false;
                    lock (jobList)
                    {
                        removedByCleanup = CleanupCompletedJobs(jobList, DateTime.Now);
                    }
                    if (removedByCleanup)
                    {
                        PersistJobHistory();
                    }

                    Log($"[{clientIp}] 打印任务完成：job-id={job.JobId}, 打印机={printerName}, 耗时={duration:F2} 秒");
                }
                catch (OperationCanceledException)
                {
                    lock (jobList)
                    {
                        job.JobState = 7;
                        job.CompletedTime = DateTime.Now;
                        job.ErrorMessage = "作业已取消";
                        job.ProcessingHoldUntil = DateTime.Now.AddMilliseconds(ProcessingStateHoldMilliseconds);
                    }
                    PersistJobHistory();
                    ExtendQueueProcessingHold(queueName);
                    Log($"[{clientIp}] 打印任务被取消：job-id={job.JobId}");
                }
                catch (Exception ex)
                {
                    lock (jobList)
                    {
                        job.JobState = 6;
                        job.CompletedTime = DateTime.Now;
                        job.ErrorMessage = ex.Message;
                        job.ProcessingHoldUntil = DateTime.Now.AddMilliseconds(ProcessingStateHoldMilliseconds);
                    }
                    PersistJobHistory();
                    ExtendQueueProcessingHold(queueName);
                    Log($"[{clientIp}] 打印任务失败：job-id={job.JobId}, 错误={ex.Message}");
                }
                finally
                {
                    _jobCancellationTokens.TryRemove(job.JobId, out _);
                    timeoutCts?.Dispose();
                    if (releaseLock)
                    {
                        queueLock?.Release();
                    }
                }
            });
        }

        private string BuildPrinterUri(string queueName)
        {
            var prefix = GetDisplayBasePrefix() ?? _config.BasePrefix ?? string.Empty;
            if (!prefix.EndsWith("/", StringComparison.Ordinal))
            {
                prefix += "/";
            }

            var trimmedQueue = string.IsNullOrWhiteSpace(queueName) ? string.Empty : queueName.Trim().Trim('/');
            return string.IsNullOrEmpty(trimmedQueue)
                ? prefix.TrimEnd('/')
                : $"{prefix.TrimEnd('/')}/{trimmedQueue}";
        }

        public string DisplayBasePrefix => GetDisplayBasePrefix();

        private string GetDisplayBasePrefix()
        {
            string basePrefix;
            lock (_configLock)
            {
                basePrefix = _config.BasePrefix;
            }
            return NormalizeBasePrefix(basePrefix);
        }

        private string NormalizeBasePrefix(string prefix)
        {
            if (string.IsNullOrWhiteSpace(prefix))
            {
                return prefix;
            }

            var sanitized = prefix;
            var containsWildcardPlus = prefix.Contains("://+");
            var containsWildcardStar = prefix.Contains("://*");
            var containsWildcardAny = prefix.Contains("://0.0.0.0");
            if (containsWildcardPlus || containsWildcardStar)
            {
                sanitized = sanitized.Replace("://+", "://localhost").Replace("://*", "://localhost");
            }

            if (!Uri.TryCreate(sanitized, UriKind.Absolute, out var uri))
            {
                return prefix;
            }

            var host = uri.Host;
            var isWildcardHost = containsWildcardPlus || containsWildcardStar || containsWildcardAny ||
                                 host == "+" || host == "*" || host == "0.0.0.0";

            if (isWildcardHost)
            {
                var localIps = GetLocalIpAddresses();
                var preferred = localIps.FirstOrDefault(ip => !ip.StartsWith("169.254.", StringComparison.Ordinal));
                host = preferred ?? localIps.FirstOrDefault() ?? "127.0.0.1";
            }

            var builder = new UriBuilder(uri)
            {
                Host = host
            };

            var normalized = builder.Uri.ToString();
            if (!normalized.EndsWith("/", StringComparison.Ordinal))
            {
                normalized += "/";
            }
            return normalized;
        }

        private void LoadJobHistory()
        {
            try
            {
                if (!File.Exists(_jobHistoryFile))
                {
                    return;
                }

                using (var stream = File.OpenRead(_jobHistoryFile))
                {
                    if (stream.Length == 0)
                    {
                        return;
                    }

                    var serializer = new DataContractJsonSerializer(typeof(List<CompletedJobSnapshot>));
                    if (!(serializer.ReadObject(stream) is List<CompletedJobSnapshot> snapshots) || snapshots.Count == 0)
                    {
                        return;
                    }

                    var now = DateTime.Now;
                    var maxId = 0;
                    foreach (var snapshot in snapshots)
                    {
                        if (snapshot == null || snapshot.JobId <= 0)
                        {
                            continue;
                        }

                        var queueName = string.IsNullOrWhiteSpace(snapshot.QueueName) ? "default" : snapshot.QueueName;
                        var job = new CompletedJob
                        {
                            JobId = snapshot.JobId,
                            JobState = snapshot.JobState,
                            JobUri = snapshot.JobUri,
                            JobName = snapshot.JobName,
                            UserName = snapshot.UserName,
                            ClientIp = snapshot.ClientIp,
                            QueueName = queueName,
                            PrinterName = snapshot.PrinterName,
                            DocumentFormat = snapshot.DocumentFormat,
                            DocumentSizeBytes = snapshot.DocumentSizeBytes,
                            PageCount = snapshot.PageCount,
                            PayloadPath = snapshot.PayloadPath,
                            CanReplay = snapshot.CanReplay && !string.IsNullOrEmpty(snapshot.PayloadPath) && File.Exists(snapshot.PayloadPath),
                            ErrorMessage = snapshot.ErrorMessage,
                            CompletedTime = snapshot.CompletedTime == default(DateTime) ? DateTime.MaxValue : snapshot.CompletedTime,
                            CreatedTime = snapshot.CreatedTime == default(DateTime) ? now : snapshot.CreatedTime,
                            ProcessingHoldUntil = now
                        };

                        // 使用当前前缀重新构建可访问的 URI，避免显示 localhost/+ 等占位符
                        if (string.IsNullOrWhiteSpace(job.JobUri) ||
                            job.JobUri.Contains("://+") ||
                            job.JobUri.Contains("://*") ||
                            job.JobUri.Contains("://localhost"))
                        {
                            job.JobUri = BuildPrinterUri(queueName).TrimEnd('/') + $"/{job.JobId}";
                        }

                        var jobList = _completedJobs.GetOrAdd(queueName, _ => new List<CompletedJob>());
                        lock (jobList)
                        {
                            if (jobList.All(j => j.JobId != job.JobId))
                            {
                                jobList.Add(job);
                            }
                        }

                        _jobIdToQueue[job.JobId] = queueName;
                        if (job.JobId > maxId)
                        {
                            maxId = job.JobId;
                        }
                    }

                    if (maxId >= _nextJobId)
                    {
                        _nextJobId = maxId + 1;
                    }
                }
            }
            catch (Exception ex)
            {
                Log($"加载历史作业失败：{ex.Message}");
            }
        }

        private void PersistJobHistory()
        {
            try
            {
                var snapshots = new List<CompletedJobSnapshot>();
                foreach (var kvp in _completedJobs)
                {
                    var queueName = kvp.Key;
                    var jobList = kvp.Value;
                    lock (jobList)
                    {
                        foreach (var job in jobList)
                        {
                            snapshots.Add(new CompletedJobSnapshot
                            {
                                JobId = job.JobId,
                                JobState = job.JobState,
                                JobUri = job.JobUri,
                                JobName = job.JobName,
                                UserName = job.UserName,
                                ClientIp = job.ClientIp,
                                QueueName = queueName,
                                PrinterName = job.PrinterName,
                                DocumentFormat = job.DocumentFormat,
                                DocumentSizeBytes = job.DocumentSizeBytes,
                                PageCount = job.PageCount,
                                PayloadPath = job.PayloadPath,
                                CanReplay = job.CanReplay,
                                ErrorMessage = job.ErrorMessage,
                                CompletedTime = job.CompletedTime,
                                CreatedTime = job.CreatedTime
                            });
                        }
                    }
                }

                var serializer = new DataContractJsonSerializer(typeof(List<CompletedJobSnapshot>));
                using (var ms = new MemoryStream())
                {
                    serializer.WriteObject(ms, snapshots);
                    lock (_jobHistoryLock)
                    {
                        File.WriteAllBytes(_jobHistoryFile, ms.ToArray());
                    }
                }
            }
            catch (Exception ex)
            {
                Log($"保存作业历史失败：{ex.Message}");
            }
        }

        private bool CleanupCompletedJobs(List<CompletedJob> jobList, DateTime now)
        {
            if (jobList == null || jobList.Count <= MaxJobsPerQueue)
            {
                return false;
            }

            var removable = jobList
                .Where(j => j.JobState != 5)
                .OrderBy(j =>
                {
                    if (j.CompletedTime != DateTime.MaxValue)
                    {
                        return j.CompletedTime;
                    }
                    return j.CreatedTime;
                })
                .ToList();

            var removed = false;
            foreach (var job in removable)
            {
                if (jobList.Count <= MaxJobsPerQueue)
                {
                    break;
                }
                jobList.Remove(job);
                _jobIdToQueue.TryRemove(job.JobId, out _);
                CleanupJobResources(job);
                removed = true;
            }

            return removed;
        }

        private static string ExtractQueueName(Uri url)
        {
            if (url == null)
            {
                return string.Empty;
            }

            var segments = url.AbsolutePath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
            return segments.Length >= 2 ? segments[1] : string.Empty;
        }

        public void Dispose()
        {
            _cts.Cancel();

            try
            {
                _listenerLoop.Wait(TimeSpan.FromSeconds(2));
            }
            catch (AggregateException)
            {
            }

            try
            {
                _listener.Stop();
            }
            catch
            {
                // 忽略停止时的错误
            }

            // 停止 80 端口监听循环
            try
            {
                if (_port80ListenerLoop != null && !_port80ListenerLoop.IsCompleted)
                {
                    _port80ListenerLoop.Wait(TimeSpan.FromSeconds(2));
                }
            }
            catch
            {
                // 忽略停止时的错误
            }

            // 停止并释放 80 端口监听
            if (_port80Listener != null)
            {
                try
                {
                    if (_port80Listener.IsListening)
                    {
                        _port80Listener.Stop();
                    }
                }
                catch
                {
                    // 忽略停止时的错误
                }
                try
                {
                    _port80Listener.Close();
                }
                catch
                {
                    // 忽略关闭时的错误
                }
                _port80Listener = null;
            }

            // 停止私有 UDP 发现服务
            try
            {
                _privateDiscovery?.Stop();
            }
            catch (Exception ex)
            {
                Log($"停止私有发现服务时发生错误：{ex.Message}");
            }

            // 停止mDNS服务发现
            try
            {
                _mdnsDiscovery?.Stop();
                _mdnsDiscovery?.Dispose();
                _mdnsDiscovery = null;
            }
            catch (Exception ex)
            {
                Log($"停止mDNS服务发现时发生错误：{ex.Message}");
            }

            _listener.Close();
            _cts.Dispose();
            lock (_serviceStateLock)
            {
                _ippServiceEnabled = false;
            }
            NotifyIppServiceStateChanged(false);
            Log("监听已停止。");
        }
        
        /// <summary>
        /// 重新加载配置（热更新）
        /// </summary>
        public void ReloadConfig()
        {
            string oldBasePrefix = null;
            bool needsRestart = false;
            
            lock (_configLock)
            {
                try
                {
                    // 保存旧的 BasePrefix，用于检测是否需要重启
                    oldBasePrefix = _config?.BasePrefix;
                    
                    // 从配置文件重新加载
                    var settings = Configuration.AppSettingsStore.Load();
                    var newConfig = ServerConfig.FromUserInput(settings.BasePrefix, settings.PrinterMappings);
                    
                    // 更新IP/MAC限制配置
                    lock (_restrictionLock)
                    {
                        _ipMacRestriction = settings.IpMacRestriction ?? Configuration.IpMacRestriction.CreateDefault();
                    }

                    // 更新私有 UDP 发现服务的共享打印机配置
                    try
                    {
                        _privateDiscovery?.UpdateConfig(newConfig.BasePrefix, newConfig.QueueToPrinter, GetTcpTasks());
                    }
                    catch (Exception ex)
                    {
                        Log($"更新私有发现服务配置失败：{ex.Message}");
                    }
                    
                    // 检测端口或路径是否改变
                    if (oldBasePrefix != null && _listener.IsListening)
                    {
                        var oldPort = ExtractPortFromPrefix(oldBasePrefix);
                        var oldPath = ExtractPathFromPrefix(oldBasePrefix);
                        var newPort = ExtractPortFromPrefix(newConfig.BasePrefix);
                        var newPath = ExtractPathFromPrefix(newConfig.BasePrefix);
                        
                        if (oldPort != newPort || oldPath != newPath)
                        {
                            needsRestart = true;
                            Log($"检测到端口或路径改变（{oldPort}{oldPath} -> {newPort}{newPath}），需要重启服务");
                        }
                    }
                    
                    // 更新配置
                    _config = newConfig;
                    
                    // 更新AdminWebUI的配置引用
                    _adminWebUI?.UpdateConfig(_config);
                    
                    // 更新mDNS服务发现（如果启用）
                    if (_enableMdnsDiscovery && _mdnsDiscovery != null)
                    {
                        UpdateMdnsDiscovery();
                    }
                    
                    if (needsRestart)
                    {
                        // 在后台任务中重启服务，避免阻塞当前请求
                        _ = Task.Run(() => RestartListenerAsync());
                    }
                    else
                    {
                        Log("配置已重新加载（热更新）");
                    }
                }
                catch (Exception ex)
                {
                    Log($"重新加载配置失败：{ex.Message}");
                }
            }
        }
        
        /// <summary>
        /// 重启 HttpListener（用于端口或路径改变时）
        /// </summary>
        private async Task RestartListenerAsync()
        {
            try
            {
                Log("正在重启服务以应用新配置...");
                
                // 停止当前的监听循环
                var oldCts = _cts;
                oldCts.Cancel();
                
                // 停止 HttpListener（这会中断 GetContextAsync，使监听循环退出）
                if (_listener.IsListening)
                {
                    _listener.Stop();
                }

                // 停止 80 端口监听（如果已启用）
                if (_port80Listener != null && _port80Listener.IsListening)
                {
                    try
                    {
                        _port80Listener.Stop();
                    }
                    catch
                    {
                        // 忽略停止错误
                    }
                }
                
                // 等待监听循环结束（最多等待 3 秒）
                try
                {
                    var timeoutTask = Task.Delay(TimeSpan.FromSeconds(3));
                    var tasks = new List<Task>();
                    if (_listenerLoop != null)
                    {
                        tasks.Add(_listenerLoop);
                    }
                    if (_port80ListenerLoop != null)
                    {
                        tasks.Add(_port80ListenerLoop);
                    }

                    if (tasks.Count > 0)
                    {
                        var combined = Task.WhenAll(tasks);
                        var completedTask = await Task.WhenAny(combined, timeoutTask).ConfigureAwait(false);
                        if (completedTask == timeoutTask)
                        {
                            Log("警告：监听循环未在预期时间内结束，继续执行重启");
                        }
                    }
                }
                catch (Exception)
                {
                    // 忽略异常
                }
                
                // 清除旧的 Prefixes 并添加新的
                _listener.Prefixes.Clear();
                lock (_configLock)
                {
                    _listener.Prefixes.Add(_config.BasePrefix);
                    // 添加根路径监听前缀
                    var rootPrefix = BuildRootPrefix(_config.BasePrefix);
                    if (!string.IsNullOrEmpty(rootPrefix) && rootPrefix != _config.BasePrefix)
                    {
                        _listener.Prefixes.Add(rootPrefix);
                    }
                }
                
                // 创建新的 CancellationTokenSource
                oldCts.Dispose();
                _cts = new CancellationTokenSource();
                
                // 重新启动 HttpListener
                _listener.Start();
                
                // 重新启动监听循环
                _listenerLoop = Task.Run(() => ListenLoopAsync());
                
                // 重新尝试在 80 端口上启动重定向监听
                var port = ExtractPortFromPrefix(_config.BasePrefix);
                var path = ExtractPathFromPrefix(_config.BasePrefix);
                if (port != 80)
                {
                    TryStartPort80RedirectListener(port, path);
                }
                
                // 获取本机IP地址
                var localIps = GetLocalIpAddresses();
                
                Log("=".PadRight(60, '='));
                Log("服务已重启，新配置已生效");
                Log("=".PadRight(60, '='));
                
                if (localIps.Count > 0)
                {
                    Log($"监听地址（客户端可使用以下任一地址连接）：");
                    foreach (var ip in localIps)
                    {
                        Log($"  • http://{ip}:{port}{path}");
                        Log($"  • ipp://{ip}:{port}{path}");
                    }
                }
                else
                {
                    Log($"监听地址：{GetDisplayBasePrefix() ?? _config.BasePrefix}");
                }
                
                Log("配置已重新加载并生效");
            }
            catch (Exception ex)
            {
                Log($"重启服务失败：{ex.Message}");
                Log($"详细错误：{ex}");
            }
        }
        
        /// <summary>
        /// 更新mDNS服务发现（根据当前配置）
        /// </summary>
        private void UpdateMdnsDiscovery()
        {
            try
            {
                if (_mdnsDiscovery == null)
                {
                    return;
                }
                
                var localIps = GetLocalIpAddresses();
                var port = ExtractPortFromPrefix(_config.BasePrefix);
                
                // 停止并重新启动mDNS服务发现（最简单的方式）
                _mdnsDiscovery.Stop();
                
                // 准备新的打印机服务信息
                var printerServices = new List<Discovery.MdnsServiceDiscovery.PrinterServiceInfo>();
                lock (_configLock)
                {
                    foreach (var kvp in _config.QueueToPrinter)
                    {
                        printerServices.Add(new Discovery.MdnsServiceDiscovery.PrinterServiceInfo
                        {
                            ServiceName = $"{kvp.Value} ({kvp.Key})",
                            QueueName = kvp.Key,
                            PrinterName = kvp.Value
                        });
                    }
                }
                
                // 重新启动
                if (printerServices.Count > 0)
                {
                    _mdnsDiscovery.Start(printerServices, port, Log);
                    Log($"mDNS服务发现已更新，当前注册 {printerServices.Count} 个打印机服务");
                }
            }
            catch (Exception ex)
            {
                Log($"更新mDNS服务发现失败：{ex.Message}");
            }
        }

        public void Log(string message)
        {
            // 先存储日志用于WEB界面显示
            lock (_logsLock)
            {
                _logs.Add(new LogEntry
                {
                    Timestamp = DateTime.Now,
                    Message = message
                });
                
                // 限制日志数量，保留最新的
                if (_logs.Count > MaxLogEntries)
                {
                    _logs.RemoveRange(0, _logs.Count - MaxLogEntries);
                }
            }
            
            // 然后调用回调（通常是 AppendLog，用于GUI显示）
            // 注意：如果回调内部又调用 Log，需要防止循环调用
            _logCallback?.Invoke(message);
        }
        
        /// <summary>
        /// 获取所有日志
        /// </summary>
        public List<LogEntry> GetLogs()
        {
            lock (_logsLock)
            {
                return new List<LogEntry>(_logs);
            }
        }
        
        /// <summary>
        /// 清除所有日志
        /// </summary>
        public void ClearLogs()
        {
            lock (_logsLock)
            {
                _logs.Clear();
            }
        }
        
        /// <summary>
        /// 导出日志为文本
        /// </summary>
        public string ExportLogs()
        {
            lock (_logsLock)
            {
                var sb = new StringBuilder();
                foreach (var log in _logs)
                {
                    sb.AppendLine($"[{log.Timestamp:yyyy-MM-dd HH:mm:ss}] {log.Message}");
                }
                return sb.ToString();
            }
        }
        
        /// <summary>
        /// 日志条目
        /// </summary>
        public sealed class LogEntry
        {
            public DateTime Timestamp { get; set; }
            public string Message { get; set; }
        }
    }
}

